Merge branch 'stable-2.7'
* stable-2.7:
Set version to 2.7
Fix installation of plugins
Fix change stuck in SUBMITTED state but actually merged
Remove documentation links from admin page
NPE when deleting draft patch set when previous draft already deleted
Fix example for 'test-submit rule' in Prolog cookbook
Mark ALREADY_MERGED changes as merged in the DB and run hooks
Conflicts:
gerrit-acceptance-tests/pom.xml
gerrit-antlr/pom.xml
gerrit-cache-h2/pom.xml
gerrit-common/pom.xml
gerrit-extension-api/pom.xml
gerrit-gwtdebug/pom.xml
gerrit-gwtexpui/pom.xml
gerrit-gwtui/pom.xml
gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
gerrit-httpd/pom.xml
gerrit-launcher/pom.xml
gerrit-main/pom.xml
gerrit-openid/pom.xml
gerrit-patch-commonsnet/pom.xml
gerrit-patch-jgit/pom.xml
gerrit-pgm/pom.xml
gerrit-plugin-api/pom.xml
gerrit-plugin-archetype/pom.xml
gerrit-plugin-gwt-archetype/pom.xml
gerrit-plugin-gwtui/pom.xml
gerrit-plugin-js-archetype/pom.xml
gerrit-prettify/pom.xml
gerrit-reviewdb/pom.xml
gerrit-server/pom.xml
gerrit-sshd/pom.xml
gerrit-util-cli/pom.xml
gerrit-util-ssl/pom.xml
gerrit-war/pom.xml
plugins/commit-message-length-validator
plugins/replication
plugins/reviewnotes
pom.xml
Change-Id: Iacb961caf52032e7e445be9ed81376db849e171b
diff --git a/.buckconfig b/.buckconfig
new file mode 100644
index 0000000..171acb5
--- /dev/null
+++ b/.buckconfig
@@ -0,0 +1,20 @@
+[alias]
+ api = //:api
+ api_deploy = //tools/maven:deploy
+ api_install = //tools/maven:install
+ docs = //Documentation:html
+ download = //tools:download
+ download_sources = //tools:download_sources
+ gerrit = //:gerrit
+ eclipse = //tools/eclipse:eclipse
+ eclipse_project = //tools/eclipse:eclipse_project
+ release = //:release
+
+[buildfile]
+ includes = //tools/default.defs
+
+[java]
+ src_roots = java, resources
+
+[project]
+ ignore = .git
diff --git a/.buckversion b/.buckversion
new file mode 100644
index 0000000..4b453f2
--- /dev/null
+++ b/.buckversion
@@ -0,0 +1 @@
+5fc60079d9dbaaf8a1e7d542dcb21fd901f68245
diff --git a/.gitignore b/.gitignore
index c87c26f..1b4c29c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,17 @@
/.classpath
/.project
-/.settings
-/.settings/org.eclipse.jdt.core.prefs
/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/.settings/org.eclipse.ltk.core.refactoring.prefs
/test_site
/.idea
/gerrit-parent.iml
*.sublime-*
+/gerrit-package-plugins
+/.buckconfig.local
+/.buckd
+/buck-cache
+/buck-out
+/local.properties
+*.pyc
+/gwt-unitCache
diff --git a/.gitmodules b/.gitmodules
index 0f7fdab..2ac959c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -9,3 +9,7 @@
[submodule "plugins/commit-message-length-validator"]
path = plugins/commit-message-length-validator
url = https://gerrit.googlesource.com/plugins/commit-message-length-validator
+
+[submodule "plugins/cookbook-plugin"]
+ path = plugins/cookbook-plugin
+ url = https://gerrit.googlesource.com/plugins/cookbook-plugin
diff --git a/.pydevproject b/.pydevproject
new file mode 100644
index 0000000..be43141
--- /dev/null
+++ b/.pydevproject
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?eclipse-pydev version="1.0"?>
+
+<pydev_project>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.6.5</pydev_property>
+</pydev_project>
diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..29abf99
--- /dev/null
+++ b/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,6 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
+encoding//src/test/resources=UTF-8
+encoding/<project>=UTF-8
diff --git a/.settings/org.eclipse.core.runtime.prefs b/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..8667cfd
--- /dev/null
+++ b/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,3 @@
+#Tue Sep 02 16:59:24 PDT 2008
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..dbc83d5
--- /dev/null
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,346 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=ignore
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=16
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=0
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=0
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=2
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=true
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
+org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.join_lines_in_comments=true
+org.eclipse.jdt.core.formatter.join_wrapped_lines=true
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.lineSplit=80
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=3
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=2
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..d4218a5
--- /dev/null
+++ b/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,61 @@
+#Wed Jul 29 11:31:38 PDT 2009
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_Google Format
+formatter_settings_version=11
+org.eclipse.jdt.ui.ignorelowercasenames=true
+org.eclipse.jdt.ui.importorder=com.google;com;junit;net;org;java;javax;
+org.eclipse.jdt.ui.ondemandthreshold=99
+org.eclipse.jdt.ui.staticondemandthreshold=99
+org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/>
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=false
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.format_source_code=false
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.make_local_variable_final=true
+sp_cleanup.make_parameters_final=true
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=false
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_trailing_whitespaces=true
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_casts=false
+sp_cleanup.remove_unnecessary_nls_tags=false
+sp_cleanup.remove_unused_imports=false
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_blocks=false
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/BUCK b/BUCK
new file mode 100644
index 0000000..b1a0c75
--- /dev/null
+++ b/BUCK
@@ -0,0 +1,75 @@
+include_defs('//tools/build.defs')
+
+gerrit_war(name = 'gerrit')
+gerrit_war(name = 'chrome', ui = 'ui_chrome')
+gerrit_war(name = 'firefox', ui = 'ui_firefox')
+gerrit_war(name = 'withdocs', context = DOCS)
+gerrit_war(name = 'release', context = DOCS + ['//plugins:core.zip'])
+
+API_DEPS = [
+ ':extension-api',
+ ':extension-api-src',
+ ':plugin-api',
+ ':plugin-api-src',
+]
+
+genrule(
+ name = 'api',
+ cmd = '',
+ deps = API_DEPS,
+ out = '__fake.api__',
+)
+
+java_binary(
+ name = 'extension-api',
+ deps = [':extension-lib'],
+ visibility = ['//tools/maven:'],
+)
+
+java_library(
+ name = 'extension-lib',
+ deps = [
+ '//gerrit-extension-api:api',
+ '//lib/guice:guice',
+ '//lib/guice:guice-servlet',
+ '//lib:servlet-api-3_0',
+ ],
+ export_deps = True,
+ visibility = ['PUBLIC'],
+)
+
+genrule(
+ name = 'extension-api-src',
+ cmd = 'ln -s $(location //gerrit-extension-api:api-src) $OUT',
+ deps = ['//gerrit-extension-api:api-src'],
+ out = 'extension-api-src.jar',
+ visibility = ['//tools/maven:'],
+)
+
+PLUGIN_API = [
+ '//gerrit-server:server',
+ '//gerrit-pgm:init-api',
+ '//gerrit-sshd:sshd',
+ '//gerrit-httpd:httpd',
+]
+
+java_binary(
+ name = 'plugin-api',
+ deps = [':plugin-lib'],
+ visibility = ['//tools/maven:'],
+)
+
+java_library(
+ name = 'plugin-lib',
+ deps = PLUGIN_API + ['//lib:servlet-api-3_0'],
+ export_deps = True,
+ visibility = ['PUBLIC'],
+)
+
+java_binary(
+ name = 'plugin-api-src',
+ deps = [
+ '//gerrit-extension-api:api-src',
+ ] + [d + '-src' for d in PLUGIN_API],
+ visibility = ['//tools/maven:'],
+)
diff --git a/Documentation/BUCK b/Documentation/BUCK
new file mode 100644
index 0000000..faa2553
--- /dev/null
+++ b/Documentation/BUCK
@@ -0,0 +1,66 @@
+include_defs('//Documentation/asciidoc.defs')
+include_defs('//Documentation/config.defs')
+include_defs('//tools/git.defs')
+
+MAIN = ['//gerrit-pgm:pgm', '//gerrit-gwtui:ui_module']
+SRCS = glob(['*.txt'], excludes = ['licenses.txt'])
+HTML = [txt[0:-4] + '.html' for txt in SRCS]
+
+genrule(
+ name = 'html',
+ cmd = 'cd $TMP;' +
+ 'mkdir -p Documentation/images;' +
+ 'for s in $SRCS;do ln -s $s Documentation;done;' +
+ 'mv Documentation/*.{jpg,png} Documentation/images;' +
+ 'rm Documentation/licenses.txt;' +
+ 'cp $SRCDIR/licenses.txt LICENSES.txt;' +
+ 'zip -qr $OUT *',
+ srcs = [genfile(d) for d in HTML] +
+ glob([
+ 'images/*.jpg',
+ 'images/*.png',
+ ]) + [
+ genfile('doc.css'),
+ genfile('licenses.html'),
+ genfile('licenses.txt'),
+ ],
+ deps = [':' + d for d in HTML] + [
+ ':licenses.html',
+ ':licenses.txt',
+ ':doc.css',
+ ],
+ out = 'html.zip',
+ visibility = ['PUBLIC'],
+)
+
+genrule(
+ name = 'doc.css',
+ cmd = 'ln -s $SRCDIR/doc.css $OUT',
+ srcs = ['doc.css'],
+ out = 'doc.css',
+)
+
+genasciidoc(
+ srcs = SRCS + [genfile('licenses.txt')],
+ outs = HTML + ['licenses.html'],
+ deps = DOCUMENTATION_DEPS,
+ attributes = documentation_attributes(git_describe()),
+ backend = 'html5',
+)
+
+genrule(
+ name = 'licenses.txt',
+ cmd = '$(exe :gen_licenses) >$OUT',
+ deps = [':gen_licenses'] + MAIN,
+ out = 'licenses.txt',
+)
+
+python_binary(
+ name = 'gen_licenses',
+ main = 'gen_licenses.py',
+)
+
+python_binary(
+ name = 'replace_macros',
+ main = 'replace_macros.py',
+)
diff --git a/Documentation/Makefile b/Documentation/Makefile
deleted file mode 100644
index 59de209..0000000
--- a/Documentation/Makefile
+++ /dev/null
@@ -1,95 +0,0 @@
-# Copyright (C) 2009 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-ASCIIDOC ?= asciidoc
-ASCIIDOC_EXTRA ?=
-ASCIIDOC_VER ?= 8.6.3
-SVN ?= svn
-PUB_ROOT ?= https://gerrit-documentation.googlecode.com/svn/Documentation
-
-all: html
-
-clean:
- rm -f *.html
- rm -rf $(LOCAL_ROOT)
-
-ASCIIDOC_EXE := $(shell which $(ASCIIDOC))
-ifeq ($(wildcard $(ASCIIDOC_EXE)),)
- $(error $(ASCIIDOC) must be available)
-else
- ASCIIDOC_OK := $(shell expr `asciidoc --version | cut -f2 -d' '` \>= $(ASCIIDOC_VER))
- ifeq ($(ASCIIDOC_OK),0)
- $(error $(ASCIIDOC) version $(ASCIIDOC_VER) or higher is required)
- endif
-endif
-
-ifeq ($(origin VERSION), undefined)
- VERSION := $(shell ./GEN-DOC-VERSION 2>/dev/null)
-endif
-
-DOC_HTML := $(patsubst %.txt,%.html,$(wildcard *.txt))
-LOCAL_ROOT := .published
-COMMIT := $(shell git describe HEAD | sed s/^v//)
-PUB_DIR := $(PUB_ROOT)/$(VERSION)
-PRIOR = PRIOR
-
-ifeq ($(VERSION),)
- REVISION = $(COMMIT)
-else
- ifeq ($(VERSION),$(COMMIT))
- REVISION := $(VERSION)
- else
- REVISION := $(VERSION) (from v$(COMMIT))
- endif
-endif
-
-html: $(DOC_HTML)
-
-update: html
-ifeq ($(VERSION),)
- ./GEN-DOC-VERSION
-endif
- @-rm -rf $(LOCAL_ROOT)
- @echo "Checking out current $(VERSION)"
- @if ! $(SVN) checkout $(PUB_DIR) $(LOCAL_ROOT) 2>/dev/null ; then \
- echo "Copying $(PRIOR) to $(VERSION) ..." && \
- $(SVN) cp -m "Create $(VERSION) documentation" $(PUB_ROOT)/$(PRIOR) $(PUB_DIR) && \
- $(SVN) checkout $(PUB_DIR) $(LOCAL_ROOT) ; \
- fi
- @rm -f $(LOCAL_ROOT)/*.html
- @cp *.html $(LOCAL_ROOT)
- @cd $(LOCAL_ROOT) && \
- r=`$(SVN) status | perl -ne 'print if s/^! *//' ` && \
- if [ -n "$$r" ]; then $(SVN) rm $$r; fi && \
- a=`$(SVN) status | perl -ne 'print if s/^\? *//' ` && \
- if [ -n "$$a" ]; then \
- $(SVN) add $$a && \
- $(SVN) propset svn:mime-type text/html $$a ; \
- fi && \
- echo "Committing $(VERSION) at v$(COMMIT)" && \
- $(SVN) commit -m "Updated $(VERSION) documentation to v$(COMMIT)"
- @-rm -rf $(LOCAL_ROOT)
-
-$(DOC_HTML): %.html : %.txt
- @echo "FORMAT $@"
- @rm -f $@+ $@
- @$(ASCIIDOC) -a toc \
- -a data-uri \
- -a 'revision=$(REVISION)' \
- -a 'newline=\n' \
- -b xhtml11 \
- -f asciidoc.conf \
- $(ASCIIDOC_EXTRA) \
- -o $@+ $<
- @mv $@+ $@
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index ab32d78..db806cc 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -348,7 +348,7 @@
This is where the Gerrit configuration of each project is residing. This
branch contains several files of importance: +project.config+, +groups+ and
-+rules.pl+. Torgether they control access and behaviour during the change
++rules.pl+. Torgether they control access and behavior during the change
review process.
@@ -374,7 +374,6 @@
These are references with added functionality to them compared to a regular
git push operation.
-
refs/for/<branch ref>
^^^^^^^^^^^^^^^^^^^^^
@@ -410,10 +409,6 @@
Gerrit has several permission categories that can be granted to groups
within projects, enabling functionality for that group's members.
-With the release of the Gerrit 2.2.x series, the web GUI for ACL
-configuration was rewritten from scratch. Use this
-<<conversion_table,table>> to better understand the access rights
-conversions from the Gerrit 2.1.x to the Gerrit 2.2.x series.
[[category_abandon]]
@@ -424,8 +419,9 @@
to projects in Gerrit. It can give permission to abandon a specific
change to a given ref.
-This also grants the permission to restore a change if the change
-can be uploaded.
+This also grants the permission to restore a change if the user also
+has link:#category_push[push permission] on the change's destination
+ref.
[[category_create]]
@@ -604,7 +600,7 @@
[[category_push_merge]]
Push Merge Commits
-~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~
The `Push Merge Commit` access right permits the user to upload merge
commits. It's an add-on to the <<category_push,Push>> access right, and
@@ -752,7 +748,8 @@
For every configured label `My-Name` in the project, there is a
corresponding permission `label-My-Name` with a range corresponding to
-the defined values.
+the defined values. There is also a corresponding `labelAs-My-Name`
+permission that enables editing another user's label.
Gerrit comes pre-configured with a default 'Code-Review' label that can
be granted to groups within projects, enabling functionality for that
@@ -875,7 +872,7 @@
* xref:category_push_merge[`Push merge commit`] to 'refs/for/refs/heads/*'
* xref:category_forge_author[`Forge Author Identity`] to 'refs/heads/*'
* link:config-labels.html#label_Code-Review[`Label: Code-Review`] with range '-2' to '+2' for 'refs/heads/*'
-* link:config-labels.html#label_Verified[`Label: Verify`] with range '-1' to '+1' for 'refs/heads/*'
+* link:config-labels.html#label_Verified[`Label: Verified`] with range '-1' to '+1' for 'refs/heads/*'
* xref:category_submit[`Submit`]
If the project is small or the developers are seasoned it might make
@@ -903,15 +900,15 @@
* An unstable build (tests fails)
* A failed build
-Usually the range chosen for this verdict is the verify label. Depending on
+Usually the range chosen for this verdict is the `Verified` label. Depending on
the size of your project and discipline of involved developers you might want
-to limit access right to the +1 `Verify` label to the CI system only. That
+to limit access right to the +1 `Verified` label to the CI system only. That
way it's guaranteed that submitted commits always get built and pass tests
successfully.
If the build doesn't complete successfully the CI system can set the
-`Verify` label to -1. However that means that a failed build will block
-submit of the change even if someone else sets `Verify` +1. Depending on the
+`Verified` label to -1. However that means that a failed build will block
+submit of the change even if someone else sets `Verified` +1. Depending on the
project and how much the CI system can be trusted for accurate results, a
blocking label might not be feasible. A recommended alternative is to set the
label `Code-review` to -1 instead, as it isn't a blocking label but still
@@ -928,7 +925,7 @@
* xref:category_read[`Read`] on 'refs/heads/\*' and 'refs/tags/*'
* link:config-labels.html#label_Code-Review[`Label: Code-Review`] with range '-1' to '0' for 'refs/heads/*'
-* link:config-labels.html#label_Verified[`Label: Verify`] with range '0' to '+1' for 'refs/heads/*'
+* link:config-labels.html#label_Verified[`Label: Verified`] with range '0' to '+1' for 'refs/heads/*'
Optional access rights to grant:
@@ -1118,43 +1115,6 @@
label-Release-Process = -1..+1 group Release Engineers
====
-
-[[conversion_table]]
-Conversion table from 2.1.x series to 2.2.x series
---------------------------------------------------
-
-[options="header"]
-|=================================================================================
-|Gerrit 2.1.x |Gerrit 2.2.x
-|Code review |link:config-labels.html#label_Code-Review[Label: Code-Review]
-|Verify |link:config-labels.html#label_Verified[Label: Verify]
-|Forge Identity +1 |Forge <<category_forge_author,author>> identity
-|Forge Identity +2 |Forge <<category_forge_committer,committer>> & <<category_forge_author,author>> identity
-|Forge Identity +3 |Forge <<category_forge_server,server>> & <<category_forge_committer,committer>> & <<category_forge_author,author>> identity
-|Owner |<<category_owner,Owner>>
-|Push branch +1 |<<category_push_direct,Push>>
-|Push branch +2 |<<category_create,Create reference>> & <<category_push_direct,Push>>
-|Push branch +3 |<<category_push_direct,Push>> (with force) & <<category_create,Create reference>>
-|Push tag +1 & Push Branch +2 |No support to limit to push signed tag
-|Push tag +2 & Push Branch +2 |<<category_push_annotated,Push annotated tag>>
-|Push Branch +2 (refs/tags/*) |<<category_create,Create reference>> (refs/tags/...)
-|Push Branch +3 (refs/tags/*) |<<category_push_direct,Push>> (with force on refs/tags/...)
-|Read +1 |<<category_read,Read>>
-|Read +2 |<<category_read,Read>> & <<category_push_review,Push>> (refs/for/refs/...)
-|Read +3 |<<category_read,Read>> & <<category_push_review,Push>> (refs/for/refs/...) & <<category_push_merge,Push Merge Commit>>
-|Submit |<<category_submit,Submit>>
-|=================================================================================
-
-
-[NOTE]
-In Gerrit 2.2.x, the way to set permissions for upload has changed entirely.
-To upload a change for review is no longer a separate permission type,
-instead you grant ordinary push permissions to the actual
-receiving reference. In practice this means that you set push permissions
-on `refs/for/refs/heads/<branch>` rather than permissions to upload changes
-on `refs/heads/<branch>`.
-
-
[[global_capabilities]]
Global Capabilities
-------------------
@@ -1176,6 +1136,13 @@
Below you find a list of capabilities available:
+[[capability_accessDatabase]]
+Access Database
+~~~~~~~~~~~~~~~
+
+Allow users to access the database using the `gsql` command.
+
+
[[capability_administrateServer]]
Administrate Server
~~~~~~~~~~~~~~~~~~~
@@ -1238,6 +1205,14 @@
you need the <<capability_viewCaches,view caches capability>>.
+[[capability_generateHttpPassword]]
+Generate HTTP Password
+~~~~~~~~~~~~~~~~~~~~~~
+
+Allow the user to generate HTTP passwords for other users. Typically this would
+be assigned to a non-interactive users group.
+
+
[[capability_kill]]
Kill Task
~~~~~~~~~
@@ -1283,8 +1258,8 @@
Allow site administrators to configure the query limit for users to
be above the default hard-coded value of 500. Administrators can add
-a global block to `All-Projects` with group(s) that
-should have different limits:
+a global block to `All-Projects` with group(s) that should have different
+limits.
When applying a query limit to a user the largest value granted by
any of their groups is used.
@@ -1293,11 +1268,25 @@
command, but also to the web UI results pagination size.
-[[capability_accessDatabase]]
-Access Database
-~~~~~~~~~~~~~~~
+[[capability_runAs]]
+Run As
+~~~~~~
-Allow users to access the database using the `gsql` command.
+Allow users to impersonate any other user with the `X-Gerrit-RunAs`
+HTTP header on REST API calls, or the link:cmd-suexec.html[suexec]
+SSH command.
+
+When impersonating an administrator the Administrate Server capability
+is not honored. This security feature tries to prevent a role with
+Run As capability from modifying the access controls in All-Projects,
+however modification may still be possible if the impersonated user
+has permission to push or submit changes on `refs/meta/config`. Run
+As also blocks using most capabilities including Create User, Run
+Garbage Collection, etc., unless the capability is also explicitly
+granted to a group the administrator is a member of.
+
+Administrators do not automatically inherit this capability; it must
+be explicitly granted.
[[capability_runGC]]
@@ -1308,14 +1297,6 @@
all projects.
-[[capability_startReplication]]
-Start Replication
-~~~~~~~~~~~~~~~~~
-
-Allow access to execute `replication start` command, if the
-replication plugin is installed on the server.
-
-
[[capability_streamEvents]]
Stream Events
~~~~~~~~~~~~~
diff --git a/Documentation/asciidoc.conf b/Documentation/asciidoc.conf
deleted file mode 100644
index 2fe6213..0000000
--- a/Documentation/asciidoc.conf
+++ /dev/null
@@ -1,29 +0,0 @@
-[attributes]
-asterisk=*
-plus=+
-caret=^
-startsb=[
-endsb=]
-tilde=~
-
-[specialsections]
-GERRIT=gerrituplink
-
-[gerrituplink]
-<hr style="
- height: 2px;
- color: silver;
- margin-top: 1.2em;
- margin-bottom: 0.5em;
-">
-
-[macros]
-(?u)^(?P<name>get)::(?P<target>\S*?)$=#
-
-[get-blockmacro]
-<a id="{target}" onmousedown="javascript:
- var i = document.URL.lastIndexOf('/Documentation/');
- var url = document.URL.substring(0, i) + '{target}';
- document.getElementById('{target}').href = url;">
- GET {target} HTTP/1.0
-</a>
diff --git a/Documentation/asciidoc.defs b/Documentation/asciidoc.defs
new file mode 100644
index 0000000..8279847
--- /dev/null
+++ b/Documentation/asciidoc.defs
@@ -0,0 +1,61 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+def genasciidoc(
+ srcs = [],
+ outs = [],
+ deps = {},
+ attributes = [],
+ backend = None,
+ visibility = []):
+ EXPN = '.expn'
+
+ asciidoc = ['$(exe //lib/asciidoctor:asciidoc)']
+ if backend:
+ asciidoc.extend(['-b', backend])
+ for attribute in attributes:
+ asciidoc.extend(['-a', attribute])
+ asciidoc.extend(['-o', '$OUT'])
+
+ for p in zip(srcs, outs):
+ src, out = p
+ dep = deps.get(src) or []
+
+ tx = []
+ fn = src
+ if fn.startswith('BUCKGEN:') :
+ fn = src[8:]
+ tx = [':' + fn]
+ ex = fn + EXPN
+
+ genrule(
+ name = ex,
+ cmd = '$(exe :replace_macros) --suffix=' + EXPN +
+ ' -s $SRCDIR/%s' % fn +
+ ' -o $OUT',
+ srcs = [src],
+ deps = tx + [':replace_macros'],
+ out = ex,
+ )
+ genrule(
+ name = out,
+ cmd = ' '.join(asciidoc + ['$SRCDIR/' + ex]),
+ srcs = [genfile(ex)] + [genfile(n + EXPN) for n in dep],
+ deps = [':' + n + EXPN for n in dep] + [
+ ':' + ex,
+ '//lib/asciidoctor:asciidoc',
+ ],
+ out = out,
+ visibility = visibility,
+ )
diff --git a/Documentation/cmd-cherry-pick.txt b/Documentation/cmd-cherry-pick.txt
index d051a9a..15a8524 100644
--- a/Documentation/cmd-cherry-pick.txt
+++ b/Documentation/cmd-cherry-pick.txt
@@ -39,7 +39,7 @@
====
$ scp -p -P 29418 john.doe@review.example.com:bin/gerrit-cherry-pick ~/bin/
- $ curl -o ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
+ $ curl -Lo ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
====
GERRIT
diff --git a/Documentation/cmd-create-account.txt b/Documentation/cmd-create-account.txt
index 85b9b56..3ecb764 100644
--- a/Documentation/cmd-create-account.txt
+++ b/Documentation/cmd-create-account.txt
@@ -3,7 +3,7 @@
NAME
----
-gerrit create-account - Create a new batch/role account.
+gerrit create-account - Create a new user account.
SYNOPSIS
--------
@@ -24,6 +24,10 @@
used for batch/role access, such as from an automated build system
or event monitoring over link:cmd-stream-events.html[gerrit stream-events].
+Note, however, that in this case the account is not implicitly added
+to the 'Non-Interactive Users' group. The account must be explicitly
+added to the group with the `--group` option.
+
If LDAP authentication is being used, the user account is created
without checking the LDAP directory. Consequently users can be
created in Gerrit that do not exist in the underlying LDAP directory.
@@ -67,10 +71,11 @@
EXAMPLES
--------
-Create a new user account called `watcher`:
+Create a new batch/role access user account called `watcher` in
+the 'Non-Interactive Users' group.
====
- $ cat ~/.ssh/id_watcher.pub | ssh -p 29418 review.example.com gerrit create-account --ssh-key - watcher
+ $ cat ~/.ssh/id_watcher.pub | ssh -p 29418 review.example.com gerrit create-account --group "'Non-Interactive Users'" --ssh-key - watcher
====
GERRIT
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index d0e56fd..e3ad834 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -21,6 +21,7 @@
[--require-change-id | --id]
[[--branch <REF> | -b <REF>] ...]
[--empty-commit]
+ [--max-object-size-limit <N>]
{ <NAME> | --name <NAME> }
DESCRIPTION
@@ -108,6 +109,7 @@
+
* FAST_FORWARD_ONLY: produces a strictly linear history.
* MERGE_IF_NECESSARY: create a merge commit when required.
+* REBASE_IF_NECESSARY: rebase the commit when required.
* MERGE_ALWAYS: always create a merge commit.
* CHERRY_PICK: always cherry-pick the commit.
@@ -144,6 +146,15 @@
Creates an initial empty commit for the Git repository of the
project that is newly created.
+--max-object-size-limit::
+ Define maximum Git object size for this project. Pushes containing an
+ object larger than this limit will be rejected. This can be used to
+ further limit the global
+ link:config-gerrit.html#receive.maxObjectSizeLimit[receive.maxObjectSizeLimit]
+ and cannot be used to increase that globally set limit.
++
+Common unit suffixes of 'k', 'm', or 'g' are supported.
+
EXAMPLES
--------
diff --git a/Documentation/cmd-gsql.txt b/Documentation/cmd-gsql.txt
index 7ddc41f..3c1fd31 100644
--- a/Documentation/cmd-gsql.txt
+++ b/Documentation/cmd-gsql.txt
@@ -9,7 +9,7 @@
--------
[verse]
'ssh' -p <port> <host> 'gerrit gsql'
- [--format {PRETTY | JSON}]
+ [--format {PRETTY | JSON | JSON_SINGLE}]
[-c QUERY]
DESCRIPTION
@@ -26,6 +26,8 @@
for reading by a human on a sufficiently wide terminal.
In JSON mode records are output as JSON objects using the
column names as the property names, one object per line.
+ In JSON_SINGLE mode the whole result set is output as a
+ single JSON object.
-c::
Execute the single query statement supplied, and then exit.
@@ -38,7 +40,8 @@
SCRIPTING
---------
-Intended for interactive use only, unless format is JSON.
+Intended for interactive use only, unless format is JSON, or
+JSON_SINGLE.
EXAMPLES
--------
diff --git a/Documentation/cmd-hook-commit-msg.txt b/Documentation/cmd-hook-commit-msg.txt
index bd602c1..c0c1e6c 100644
--- a/Documentation/cmd-hook-commit-msg.txt
+++ b/Documentation/cmd-hook-commit-msg.txt
@@ -1,17 +1,21 @@
commit-msg Hook
===============
+
NAME
----
+
+
commit-msg - Edit commit messages to insert a `Change-Id` tag.
DESCRIPTION
-----------
+
A Git hook automatically invoked by `git commit`, and most other
commit creation tools such as `git citool` or `git gui`. The Gerrit
Code Review supplied implementation of this hook is a short shell
-script which automatically inserts a globally unique Change-Id tag
+script which automatically inserts a globally unique `Change-Id` tag
in the footer of a commit message. When present, Gerrit uses this
tag to track commits across cherry-picks and rebases.
@@ -40,28 +44,33 @@
----
The hook implementation is reasonably intelligent at inserting the
-Change-Id line before any Signed-off-by or Acked-by lines placed
+`Change-Id` line before any `Signed-off-by` or `Acked-by` lines placed
at the end of the commit message by the author, but if no such
lines are present then it will just insert a blank line, and add
-the Change-Id at the bottom of the message.
+the `Change-Id` at the bottom of the message.
-If a Change-Id line is already present in the message footer, the
-script will do nothing, leaving the existing Change-Id unmodified.
+If a `Change-Id` line is already present in the message footer, the
+script will do nothing, leaving the existing `Change-Id` unmodified.
This permits amending an existing commit, or allows the user to
insert the Change-Id manually after copying it from an existing
change viewed on the web.
+The `Change-Id` will not be added if `gerrit.createChangeId` is set
+to `false` in the git config.
+
OBTAINING
---------
-To obtain the 'commit-msg' script use scp, wget or curl to download it
-to your local system from your Gerrit server.
+
+
+To obtain the `commit-msg` script use `scp`, `wget` or `curl` to download
+it to your local system from your Gerrit server.
You can use either of the below commands:
====
$ scp -p -P 29418 <your username>@<your Gerrit review server>:hooks/commit-msg <local path to your git>/.git/hooks/
- $ curl -o <local path to your git>/.git/hooks/commit-msg <your Gerrit http URL>/tools/hooks/commit-msg
+ $ curl -Lo <local path to your git>/.git/hooks/commit-msg <your Gerrit http URL>/tools/hooks/commit-msg
====
A specific example of this might look something like this:
@@ -70,7 +79,7 @@
====
$ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg ~/duhproject/.git/hooks/
- $ curl -o ~/duhproject/.git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
+ $ curl -Lo ~/duhproject/.git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
====
Make sure the hook file is executable:
@@ -82,6 +91,7 @@
SEE ALSO
--------
+
* link:user-changeid.html[Change-Id Lines]
* link:http://www.kernel.org/pub/software/scm/git/docs/git-commit.html[git-commit(1)]
* link:http://www.kernel.org/pub/software/scm/git/docs/githooks.html[githooks(5)]
@@ -89,7 +99,8 @@
IMPLEMENTATION
--------------
-The hook generates unique Change-Id lines by creating a virtual
+
+The hook generates unique `Change-Id` lines by creating a virtual
commit object within the local Git repository, and obtaining the
SHA-1 hash from it. Like any other Git commit, the following
properties are included in the computation:
@@ -98,12 +109,14 @@
* SHA-1 of the parent commit
* Name, email address, timestamp of the author
* Name, email address, timestamp of the committer
-* Proposed commit message (before Change-Id was inserted)
+* Proposed commit message (before `Change-Id` was inserted)
Because the names of the tree and parent commit, as well as the
committer timestamp are included in the hash computation, the output
-Change-Id is sufficiently unique.
+`Change-Id` is sufficiently unique.
GERRIT
------
+
+
Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 780231c..3ba66a8 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -12,8 +12,8 @@
$ scp -p -P 29418 john.doe@review.example.com:bin/gerrit-cherry-pick ~/bin/
$ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg .git/hooks/
- $ curl -o ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
- $ curl -o .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
+ $ curl -Lo ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
+ $ curl -Lo .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
For more details on how to determine the correct SSH port number,
see link:user-upload.html#test_ssh[Testing Your SSH Connection].
@@ -60,6 +60,9 @@
link:cmd-ls-groups.html[gerrit ls-groups]::
List groups visible to the caller.
+link:cmd-ls-members.html[gerrit ls-members]::
+ List the membership of a group visible to the caller.
+
link:cmd-ls-projects.html[gerrit ls-projects]::
List projects visible to the caller.
@@ -97,7 +100,7 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
link:cmd-create-account.html[gerrit create-account]::
- Create a new batch/role account.
+ Create a new user account.
link:cmd-set-account.html[gerrit set-account]::
Change an account's settings.
@@ -120,6 +123,9 @@
link:cmd-gsql.html[gerrit gsql]::
Administrative interface to active database.
+link:cmd-set-members.html[gerrit set-members]::
+ Set group members.
+
link:cmd-set-project-parent.html[gerrit set-project-parent]::
Change the project permissions are inherited from.
diff --git a/Documentation/cmd-ls-members.txt b/Documentation/cmd-ls-members.txt
new file mode 100644
index 0000000..9814ff2
--- /dev/null
+++ b/Documentation/cmd-ls-members.txt
@@ -0,0 +1,64 @@
+gerrit ls-members
+================
+
+NAME
+----
+gerrit ls-members - Show members of a given group
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit ls-members GROUPNAME'
+ [--recursive]
+
+DESCRIPTION
+-----------
+Displays the members of the given group, one per line, so long as the given
+group is visible to the user. The users' id, username, full name and email are
+shown tab-separated.
+
+ACCESS
+------
+Any user who has configured an SSH key.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts. Output is either an error
+message or a heading followed by zero or more lines, one for each member of the
+group. If any field is not set, or if the field is the user's full name and the
+name is empty, "n/a" is emitted as the field value.
+
+All non-printable characters (ASCII value 31 or less) are escaped
+according to the conventions used in languages like C, Python, and Perl,
+employing standard sequences like `\n` and `\t`, and `\xNN` for all
+others. In shell scripts, the `printf` command can be used to unescape
+the output.
+
+OPTIONS
+-------
+--recursive::
+ If a member of the group is itself a group, the sub-group's
+ members are included in the list. Otherwise members of any sub-group
+ are not shown and no indication is given that a sub-group is present
+
+EXAMPLES
+--------
+
+List members of the Administrators group:
+=====
+ $ ssh -p 29418 review.example.com gerrit ls-members Administrators
+ id username full name email
+ 100000 jim Jim Bob somebody@example.com
+ 100001 johnny John Smith n/a
+ 100002 mrnoname n/a someoneelse@example.com
+=====
+
+List members of a non-existent group:
+=====
+ $ ssh -p 29418 review.example.com gerrit ls-members BadlySpelledGroup
+ Group not found or not visible
+=====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 65c21db..70213da 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -11,7 +11,6 @@
'ssh' -p <port> <host> 'gerrit review'
[--project <PROJECT> | -p <PROJECT>]
[--message <MESSAGE> | -m <MESSAGE>]
- [--force-message]
[--submit | -s]
[--abandon | --restore]
[--publish]
@@ -51,22 +50,6 @@
Optional cover letter to include as part of the message
sent to reviewers when the approval states are updated.
---force-message::
- Option which allows Gerrit to publish the --message, even
- when the labels could not be applied due to the change being
- closed.
-+
-Used by some scripts/CI-systems, where the results (or links
-to the result) are posted as a message after completion of a
-build (often together with a label-change, indicating the success
-of the build).
-+
-If the message is posted successfully, the command will return
-successfully, even if the label could not be changed.
-+
-This option will not force the message to be posted if the command
-fails because the user is not permitted to change the label.
-
--help::
-h::
Display site-specific usage information, including the
diff --git a/Documentation/cmd-set-members.txt b/Documentation/cmd-set-members.txt
new file mode 100644
index 0000000..7524893
--- /dev/null
+++ b/Documentation/cmd-set-members.txt
@@ -0,0 +1,82 @@
+gerrit set-members
+==================
+
+NAME
+----
+gerrit set-members - Set group members
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit set-members'
+ [--add USER ...]
+ [--remove USER ...]
+ [--include GROUP ...]
+ [--exclude GROUP ...]
+ [--]
+ <GROUP> ...
+
+DESCRIPTION
+-----------
+Set the group members for the specified groups.
+
+OPTIONS
+-------
+<GROUP>::
+ Required; name of the group for which the members should be set.
+ The members for multiple groups can be set at once by specifying
+ multiple groups.
+
+--add::
+-a::
+ A user that should be added to the specified groups. Multiple
+ users can be added at once by using this option multiple times.
+
+--remove::
+-r::
+ Remove this user from the specified groups. Multiple users can be
+ removed at once by using this option multiple times.
+
+--include::
+-i::
+ A group that should be included to the specified groups. Multiple
+ groups can be included at once by using this option multiple
+ times.
+
+--exclude::
+-e::
+ Exclude this group from the specified groups. Multiple groups can
+ be excluded at once by using this option multiple times.
+
+The `set-members` command is processing the options in the following
+order: `--remove`, `--exclude`, `--add`, `--include`
+
+ACCESS
+------
+Any user who has configured an SSH key.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+EXAMPLES
+--------
+
+Add alice and bob, but remove eve from the groups my-committers and
+my-verifiers.
+=====
+ $ ssh -p 29418 review.example.com gerrit set-members \
+ -a alice@example.com -a bob@example.com \
+ -r eve@example.com my-committers my-verifiers
+=====
+
+Include the group my-friends into the group my-committers, but
+exclude the included group my-testers from the group my-committers.
+=====
+ $ ssh -p 29418 review.example.com gerrit set-members \
+ -i my-friends -e my-testers my-committers
+=====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-set-project.txt b/Documentation/cmd-set-project.txt
index a0af910..af20006 100644
--- a/Documentation/cmd-set-project.txt
+++ b/Documentation/cmd-set-project.txt
@@ -16,6 +16,7 @@
[--content-merge <true|false|inherit>]
[--change-id <true|false|inherit>]
[--project-state <STATE> | --ps <STATE>]
+ [--max-object-size-limit <N>]
<NAME>
DESCRIPTION
@@ -56,6 +57,7 @@
+
* FAST_FORWARD_ONLY: produces a strictly linear history.
* MERGE_IF_NECESSARY: create a merge commit when required.
+* REBASE_IF_NECESSARY: rebase the commit when required.
* MERGE_ALWAYS: always create a merge commit.
* CHERRY_PICK: always cherry-pick the commit.
@@ -93,6 +95,15 @@
is granted, but all modification operations are disabled.
* HIDDEN: the project is not visible for those who are not owners
+--max-object-size-limit::
+ Define maximum Git object size for this project. Pushes containing an
+ object larger than this limit will be rejected. This can be used to
+ further limit the global
+ link:config-gerrit.html#receive.maxObjectSizeLimit[receive.maxObjectSizeLimit]
+ and cannot be used to increase that globally set limit.
++
+Common unit suffixes of 'k', 'm', or 'g' are supported.
+
EXAMPLES
--------
Change project `example` to be hidden, require change id, don't use content merge
@@ -105,4 +116,4 @@
GERRIT
------
-Part of link:index.html[Gerrit Code Review]
\ No newline at end of file
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-show-caches.txt b/Documentation/cmd-show-caches.txt
index 126b2a0..d426508 100644
--- a/Documentation/cmd-show-caches.txt
+++ b/Documentation/cmd-show-caches.txt
@@ -25,6 +25,10 @@
operating system, and other details about the environment
that Gerrit Code Review is running in.
+--width::
+-w::
+ Width of the output table.
+
ACCESS
------
Caller must be a member of the privileged 'Administrators' group,
diff --git a/Documentation/cmd-show-connections.txt b/Documentation/cmd-show-connections.txt
index 8404a97..ab9fadf 100644
--- a/Documentation/cmd-show-connections.txt
+++ b/Documentation/cmd-show-connections.txt
@@ -32,6 +32,11 @@
-n::
Show client hostnames as IP addresses instead of DNS hostname.
+--wide::
+-w::
+ Do not format the output to the terminal width (default of
+ 80 columns).
+
DISPLAY
-------
diff --git a/Documentation/cmd-show-queue.txt b/Documentation/cmd-show-queue.txt
index f99e342..4ab3097 100644
--- a/Documentation/cmd-show-queue.txt
+++ b/Documentation/cmd-show-queue.txt
@@ -18,7 +18,7 @@
Gerrit contains an internal scheduler, similar to cron, that it
uses to queue and dispatch both short and long term activity.
-Tasks that are completed or cancelled exit the queue very quickly
+Tasks that are completed or canceled exit the queue very quickly
once they enter this state, but it can be possible to observe tasks
in these states.
@@ -37,6 +37,13 @@
---------
Intended for interactive use only.
+OPTIONS
+-------
+--wide::
+-w::
+ Do not format the output to the terminal width (default of
+ 80 columns).
+
DISPLAY
-------
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index 6da0ef0..10689b6 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -14,7 +14,7 @@
-----------
Provides a portal into the major events occurring on the server,
-outputing activity data in real-time to the client. Events are
+outputting activity data in real-time to the client. Events are
filtered by the caller's access permissions, ensuring the caller
only receives events for changes they can view on the web, or in
the project repository.
@@ -70,7 +70,7 @@
change:: link:json.html#change[change attribute]
-patchset:: link:json.html#patchSet[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
uploader:: link:json.html#account[account attribute]
@@ -148,10 +148,19 @@
change:: link:json.html#change[change attribute]
-patchset:: link:json.html#patchSet[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
reviewer:: link:json.html#account[account attribute]
+Topic Changed
+^^^^^^^^^^^^^
+type:: "topic-changed"
+
+change:: link:json.html#change[change attribute]
+
+changer:: link:json.html#account[account attribute]
+
+oldTopic:: Topic name before it was changed.
SEE ALSO
--------
diff --git a/Documentation/cmd-suexec.txt b/Documentation/cmd-suexec.txt
index baffd53..78fc361 100644
--- a/Documentation/cmd-suexec.txt
+++ b/Documentation/cmd-suexec.txt
@@ -19,10 +19,14 @@
DESCRIPTION
-----------
-The suexec command can only be invoked by the magic user `Gerrit
-Code Review` and permits executing any other command as any other
+The suexec command permits executing any other command as any other
registered user account.
+suexec can only be invoked by the magic user `Gerrit Code Review`,
+or any user granted granted the link:access-control.html#capability_runAs[Run As]
+capability. The run as capability is permitted to be used only if
+link:config-gerrit.html[auth.enableRunAs] is true.
+
OPTIONS
-------
@@ -39,7 +43,8 @@
ACCESS
------
Caller must be the magic user Gerrit Code Review using the SSH
-daemon's host key or a key on this daemon's peer host key ring.
+daemon's host key, or a key on this daemon's peer host key ring,
+or a user granted the Run As capability.
SCRIPTING
---------
diff --git a/Documentation/cmd-version.txt b/Documentation/cmd-version.txt
index d1f94ea..aa08848 100644
--- a/Documentation/cmd-version.txt
+++ b/Documentation/cmd-version.txt
@@ -1,5 +1,5 @@
gerrit version
-================
+==============
NAME
----
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index daafcf1..339b0c0 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -119,6 +119,8 @@
Therefore, the "_LDAP" suffix in the name of this authentication type.
This authentication type can only be used under hosted daemon mode, and
the httpd.listenUrl must use https:// as the protocol.
+Optionally, certificate revocation list file can be used
+at <review-site>/etc/crl.pem. For details, see httpd.sslCrl.
+
* `LDAP`
+
@@ -140,8 +142,8 @@
<<ldap.server,ldap.server>>. In this configuration the web server
is not involved in the user authentication process.
+
-Unlike LDAP above, the username used to perform the LDAP simple bind
-request is the exact string supplied by in the dialog by the user.
+Unlike `LDAP` above, the username used to perform the LDAP simple bind
+request is the exact string supplied in the dialog by the user.
The configured <<ldap.username,ldap.username>> identity is not used to obtain
account information.
+
@@ -165,7 +167,7 @@
+
List of permitted OpenID providers. A user may only authenticate
with an OpenID that matches this list. Only used if `auth.type`
-is set to OpenID (the default).
+is set to `OpenID` (the default).
+
Patterns may be either a
link:http://download.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html[standard
@@ -178,7 +180,7 @@
[[auth.trustedOpenID]]auth.trustedOpenID::
+
List of trusted OpenID providers. Only used if `auth.type` is
-set to OpenID (the default).
+set to `OpenID` (the default).
+
In order for a user to take advantage of permissions beyond those
granted to the `Anonymous Users` and `Registered Users` groups,
@@ -196,7 +198,7 @@
[[auth.openIdDomain]]auth.openIdDomain::
+
List of allowed OpenID email address domains. Only used if
-`auth.type` is set to "OPENID" or "OPENID_SSO".
+`auth.type` is set to `OPENID` or `OPENID_SSO`.
+
Domain is case insensitive and must be in the same form as it
appears in the email address, for example, "example.com".
@@ -245,15 +247,61 @@
[[auth.openIdSsoUrl]]auth.openIdSsoUrl::
+
-The SSO entry point URL. Only used if `auth.type` was set to
-OpenID_SSO.
+The SSO entry point URL. Only used if `auth.type` is set to
+`OpenID_SSO`.
+
The "Sign In" link will send users directly to this URL.
[[auth.httpHeader]]auth.httpHeader::
+
HTTP header to trust the username from, or unset to select HTTP basic
-or digest authentication. Only used if `auth.type` is set to HTTP.
+or digest authentication. Only used if `auth.type` is set to `HTTP`.
+
+[[auth.httpDisplaynameHeader]]auth.httpDisplaynameHeader::
++
+HTTP header to retrieve the user's display name from. Only used if `auth.type`
+is set to `HTTP`.
++
+If set, Gerrit trusts and enforces the user's full name using the HTTP header
+and disables the ability to manually modify the user's full name
+from the contact information page.
+
+[[auth.httpEmailHeader]]auth.httpEmailHeader::
++
+HTTP header to retrieve the user's e-mail from. Only used if `auth.type`
+is set to `HTTP`.
++
+If set, Gerrit trusts and enforces the user's e-mail using the HTTP header
+and disables the ability to manually modify or register other e-mails
+from the contact information page.
+
+[[auth.loginUrl]]auth.loginUrl::
++
+URL to redirect a browser to after the end-user has clicked on the
+login link in the upper right corner. Only used if `auth.type` is set
+to `HTTP` or `HTTP_LDAP`.
+Organizations using an enterprise single-sign-on solution may want to
+redirect the browser to the SSO product's sign-in page for completing the
+login process and validate their credentials.
++
+If set, Gerrit allows anonymous access until the end-user performs the login
+and provides a trusted identity through the HTTP header.
+If not set, Gerrit requires the HTTP header with a trusted identity
+and returns the error page 'LoginRedirect.html' if such a header is not
+present.
+
+[[auth.loginText]]auth.loginText::
++
+Text displayed in the loginUrl link. Only used if `auth.loginUrl` is set.
++
+If not set, the "Sign In" text is used.
+
+[[auth.registerPageUrl]]auth.registerPageUrl::
++
+URL of the registration page to use when a new user logs in to Gerrit for
+the first time. Used only when `auth.type` is set to `HTTP`.
++
+If not set, the standard Gerrit registration page `/#/register/` is displayed.
[[auth.logoutUrl]]auth.logoutUrl::
+
@@ -267,14 +315,14 @@
[[auth.registerUrl]]auth.registerUrl::
+
Target for the "Register" link in the upper right corner. Used only
-when auth.type is `LDAP`.
+when `auth.type` is `LDAP`.
+
If not set, no "Register" link is displayed.
[[auth.registerText]]auth.registerText::
+
Text for the "Register" link in the upper right corner. Used only
-when auth.type is `LDAP`.
+when `auth.type` is `LDAP`.
+
If not set, defaults to "Register".
@@ -285,9 +333,19 @@
[[auth.httpPasswordUrl]]auth.httpPasswordUrl::
+
-Target for the "Obtain Password" link. Used only when auth.type is
+Target for the "Obtain Password" link. Used only when `auth.type` is
`LDAP`, `LDAP_BIND` or `CUSTOM_EXTENSION`.
+
+[[auth.switchAccountUrl]]auth.switchAccountUrl::
+
+URL to switch user identities and login as a different account than
+the currently active account. This is disabled by default except when
+`auth.type` is `OPENID` and `DEVELOPMENT_BECOME_ANY_ACCOUNT`. If set
+the "Switch Account" link is displayed next to "Sign Out".
++
+When `auth.type` does not normally enable this URL administrators may
+set this to `login/` or `$canonicalWebUrl/login`, allowing users to
+begin a new web session.
[[auth.cookiePath]]auth.cookiePath::
+
@@ -305,7 +363,7 @@
[[auth.emailFormat]]auth.emailFormat::
+
Optional format string to construct user email addresses out of
-user login names. Only used if auth.type is `HTTP`, `HTTP_LDAP`
+user login names. Only used if `auth.type` is `HTTP`, `HTTP_LDAP`
or `LDAP`.
+
This value can be set to a format string, where `{0}` is replaced
@@ -390,6 +448,18 @@
+
By default this is set to false.
+[[auth.enableRunAs]]auth.enableRunAs::
++
+If true HTTP REST APIs will accept the `X-Gerrit-RunAs` HTTP request
+header from any users granted the link:access-control.html#capability_runAs[Run As]
+capability. The header and capability permit the authenticated user
+to impersonate another account.
++
+If false the feature is disabled and cannot be re-enabled without
+editing gerrit.config and restarting the server.
++
+Default is true.
+
[[cache]]Section cache
~~~~~~~~~~~~~~~~~~~~~~
@@ -497,7 +567,7 @@
cache `"adv_bases"`::
+
Used only for push over smart HTTP when branch level access controls
-are enabled. The cache entry contains all commits that are avaliable
+are enabled. The cache entry contains all commits that are available
for the client to use as potential delta bases. Push over smart HTTP
requires two HTTP requests, and this cache tries to carry state from
the first request into the second to ensure it can complete.
@@ -671,7 +741,7 @@
Boolean to enable or disable the computation of intraline differences
when populating a diff cache entry. This flag is provided primarily
as a backdoor to disable the intraline difference feature if
-necessary. To maintain backwards compatability with prior versions,
+necessary. To maintain backwards compatibility with prior versions,
this setting will fallback to `cache.diff.intraline` if not set in the
configuration.
+
@@ -693,6 +763,28 @@
+
Default is 5 minutes.
+[[change]]Section change
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+[[change.updateDelay]]change.updateDelay::
++
+How often in seconds the web interface should poll for updates to the
+currently open change. The poller relies on the client's browser
+cache to use If-Modified-Since and respect `304 Not Modified` HTTP
+reponses. This allows for fast polls, often under 8 milliseconds.
++
+With a configured 30 second delay a server with 4900 active users will
+typically need to dedicate 1 CPU to the update check. 4900 users
+divided by an average delay of 30 seconds is 163 requests arriving per
+second. If requests are served at ~6 ms response time, 1 CPU is
+necessary to keep up with the update request traffic. On a smaller
+user base of 500 active users, the default 30 second delay is only 17
+requests per second and requires ~10% CPU.
++
+If 0 the update polling is disabled.
++
+Default is 30 seconds.
+
[[changeMerge]]Section changeMerge
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -923,7 +1015,7 @@
Largest object size, in bytes, that JGit will allocate as a
contiguous byte array. Any file revision larger than this threshold
will have to be streamed, typically requiring the use of temporary
-files under '$GIT_DIR/objects' to implement psuedo-random access
+files under '$GIT_DIR/objects' to implement pseudo-random access
during delta decompression.
+
Servers with very high traffic should set this to be larger than
@@ -948,7 +1040,7 @@
can be made by the JVM native code.
+
In server applications (such as Gerrit) that need to access many
-pack files, setting this to true risks artifically running out
+pack files, setting this to true risks artificially running out
of virtual address space, as the garbage collector cannot reclaim
unused mapped spaces fast enough.
+
@@ -1235,6 +1327,19 @@
by the system administrator, and might not even be running on the
same host as Gerrit.
+[[gerrit.installCommitMsgHookCommand]]gerrit.installCommitMsgHookCommand::
++
+Optional command to install the `commit-msg` hook. Typically of the
+form:
+----
+fetch-cmd some://url/to/commit-msg .git/hooks/commit-msg ; chmod +x .git/hooks/commit-msg
+----
++
+By default unset; falls back to using scp from the canonical SSH host,
+or curl from the canonical HTTP URL for the server. Only necessary if a
+proxy or other server/network configuration prevents clients from
+fetching from the default location.
+
[[gerrit.gitHttpUrl]]gerrit.gitHttpUrl::
+
Optional base URL for repositories available over the HTTP
@@ -1416,6 +1521,11 @@
Optional filename for the reviewer added hook, if not specified then
`reviewer-added` will be used.
+[[hooks.topicChangedHook]]hooks.topicChangedHook::
++
+Optional filename for the topic changed hook, if not specified then
+`topic-changed` will be used.
+
[[hooks.claSignedHook]]hooks.claSignedHook::
+
Optional filename for the CLA signed hook, if not specified then
@@ -1535,6 +1645,23 @@
By default, 16384 (16 K), which is sufficient for most OpenID and
other web-based single-sign-on integrations.
+[[httpd.sslCrl]]httpd.sslCrl::
++
+Path of the certificate revocation list file in PEM format. This
+crl file is optional, and available for CLIENT_SSL_CERT_LDAP
+authentication.
++
+To create and view a crl using openssl:
++
+----
+openssl ca -gencrl -out crl.pem
+openssl crl -in crl.pem -text
+----
++
+If not absolute, the path is resolved relative to `$site_path`.
++
+By default, `$site_path/etc/crl.pem`.
+
[[httpd.sslKeyStore]]httpd.sslKeyStore::
+
Path of the Java keystore containing the server's SSL certificate
@@ -1582,7 +1709,7 @@
+
Minimum number of spare threads to keep in the worker thread pool.
This number must be at least 1 larger than httpd.acceptorThreads
-multipled by the number of httpd.listenUrls configured.
+multiplied by the number of httpd.listenUrls configured.
+
By default, 5, suitable for most lower-volume traffic sites.
@@ -1623,13 +1750,81 @@
+
By default, 5 minutes.
+[[httpd.filterClass]]httpd.filterClass::
++
+Class that implements the javax.servlet.Filter interface
+for filtering any HTTP related traffic going through the Gerrit
+HTTP protocol.
+Class is loaded and configured in the Gerrit Jetty container
+and run in front of all Gerrit URL handlers, allowing the filter
+to inspect, modify, allow or reject each request.
+It needs to be provided as JAR library
+under $GERRIT_SITE/lib as it is resolved using the default Gerrit class
+loader and cannot be dynamically loaded by a plugin.
++
+Failing to load the Filter class would result in a Gerrit start-up
+failure, as this class is supposed to provide mandatory filtering
+in front of Gerrit HTTP protocol.
++
+Typical usage is in conjunction with the `auth.type=HTTP` as replacement
+of an Apache HTTP proxy layer as security enforcement on top of Gerrit
+by returning a trusted username as HTTP Header.
++
+Example of using a security library secure.jar under $GERRIT_SITE/lib
+that provides a org.anyorg.MySecureFilter Servlet Filter that enforces
+a trusted username in the `TRUSTED_USER` HTTP Header:
+
+----
+[auth]
+ type = HTTP
+ httpHeader = TRUSTED_USER
+
+[http]
+ filterClass = org.anyorg.MySecureFilter
+----
+
+[[httpd.robotsFile]]httpd.robotsFile::
++
+Location of an external robots.txt file to be used instead of the one
+bundled with the .war of the application.
++
+If not absolute, the path is resolved relative to `$site_path`.
++
+If the file doesn't exist or can't be read the default robots.txt file
+bundled with the .war will be used instead.
+
+[[index]]Section index
+~~~~~~~~~~~~~~~~~~~~~~
+
+The index section configures the secondary index.
+
+[[index.type]]index.type::
++
+Type of secondary indexing employed by Gerrit. The supported
+values are:
++
+* `LUCENE`
++
+A link:http://lucene.apache.org/[Lucene] index is used.
++
+* `SOLR`
++
+A link:http://lucene.apache.org/solr/[Solr] index is used.
++
+* `SQL`
++
+No secondary index. Not all query operators are supported. Other
+query operators are routed through the standard SQL query engine.
+
++
+By default, `SQL`.
[[ldap]]Section ldap
~~~~~~~~~~~~~~~~~~~~
LDAP integration is only enabled if `auth.type` is set to
`HTTP_LDAP`, `LDAP` or `CLIENT_SSL_CERT_LDAP`. See above for a
-detailed description of the auth.type settings and their
+detailed description of the `auth.type` settings and their
implications.
An example LDAP configuration follows, and then discussion of
@@ -1658,7 +1853,7 @@
and group membership from. Must be of the form `ldap://host` or
`ldaps://host` to bind with either a plaintext or SSL connection.
+
-If auth.type is `LDAP` this setting should use `ldaps://` to
+If `auth.type` is `LDAP` this setting should use `ldaps://` to
ensure the end user's plaintext password is transmitted only over
an encrypted connection.
@@ -1720,9 +1915,9 @@
+
Query pattern to use when searching for a user account. This may be
any valid LDAP query expression, including the standard `(&...)` and
-`(|...)` operators. If auth.type is `HTTP_LDAP` then the variable
+`(|...)` operators. If `auth.type` is `HTTP_LDAP` then the variable
`${username}` is replaced with a parameter set to the username
-that was supplied by the HTTP server. If auth.type is `LDAP` then
+that was supplied by the HTTP server. If `auth.type` is `LDAP` then
the variable `${username}` is replaced by the string entered by
the end user.
+
@@ -1834,7 +2029,7 @@
account is currently a member of. This may be any valid LDAP query
expression, including the standard `(&...)` and `(|...)` operators.
+
-If auth.type is `HTTP_LDAP` then the variable `${username}` is
+If `auth.type` is `HTTP_LDAP` then the variable `${username}` is
replaced with a parameter set to the username that was supplied
by the HTTP server. Other variables appearing in the pattern,
such as `${fooBarAttribute}`, are replaced with the value of the
@@ -1905,7 +2100,7 @@
Note the `renewTGT` property to make sure the TGT does not expire,
and `useTicketCache` to use the TGT supplied by the operating system. As
the whole point of using GSSAPI is to have passwordless authentication
-to the LDAP service, this option does not aquire a new TGT on its own.
+to the LDAP service, this option does not acquire a new TGT on its own.
On Windows servers the registry key `HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos\Parameters`
must have the DWORD value `allowtgtsessionkey` set to 1 and the account must not
@@ -2042,6 +2237,10 @@
Gerrit administrators can use this setting to prevent developers
from pushing objects which are too large to Gerrit.
+
+This setting can also be set in the `project.config` in order to further
+reduce the global setting. The project specific setting is only honored
+when it further reduces the global limit.
++
Default is zero.
+
Common unit suffixes of 'k', 'm', or 'g' are supported.
@@ -2062,7 +2261,7 @@
set update.
+
Defaults to 1, using only the main receive thread. This feature is for
-databases with very high latency that can benfit from concurrent
+databases with very high latency that can benefit from concurrent
operations when multiple changes are impacted at once.
[[receive.timeout]]receive.timeout::
@@ -2073,7 +2272,7 @@
be specified using standard time unit abbreviations ('ms', 'sec',
'min', etc.).
+
-Default is 2 minutes. If no unit is specified, millisconds
+Default is 2 minutes. If no unit is specified, milliseconds
is assumed.
@@ -2128,7 +2327,7 @@
* `USER`
+
Gerrit will set the From header to use the current user's
-Full Name and Preferred Email. This may cause messsages to be
+Full Name and Preferred Email. This may cause messages to be
classified as spam if the user's domain has SPF or DKIM enabled
and <<sendemail.smtpServer,sendemail.smtpServer>> is not a trusted
relay for that domain.
@@ -2262,28 +2461,6 @@
and text results for changes. If false, the URL is disabled and
returns 404 to clients. Default is true, enabling `/query`.
-[[site.upgradeSchemaOnStartup]]site.upgradeSchemaOnStartup::
-+
-Control whether schema upgrade should be done on Gerrit startup. The following
-values are supported:
-+
-* `OFF`
-+
-No automatic schema upgrade on startup.
-+
-* `AUTO`
-+
-Perform schema migration on startup, if necessary. If, as a result of
-schema migration, there would be any unused database objects they will
-be dropped automatically.
-+
-* `AUTO_NO_PRUNE`
-+
-Like `AUTO` but unused database objects will not be pruned.
-
-+
-The default is `OFF`.
-
[[ssh-alias]] Section ssh-alias
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2375,7 +2552,7 @@
+
If the number of threads requested for non-interactive users is larger
than the total number of threads allocated in sshd.threads, then the
-value of sshd.threads is increased to accomodate the requested value.
+value of sshd.threads is increased to accommodate the requested value.
+
By default, 0.
@@ -2608,6 +2785,20 @@
backgroundColor = 00FFFF
----
+As example, here is the theme configuration to have the old green look:
+
+----
+[theme]
+ backgroundColor = FCFEEF
+ textColor = 000000
+ trimColor = D4E9A9
+ selectionColor = FFFFCC
+ topMenuColor = D4E9A9
+ changeTableOutdatedColor = F08080
+[theme "signed-in"]
+ backgroundColor = FFFFFF
+----
+
[[trackingid]] Section trackingid
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/Documentation/config-gitweb.txt b/Documentation/config-gitweb.txt
index e5edda8..7ba15b8 100644
--- a/Documentation/config-gitweb.txt
+++ b/Documentation/config-gitweb.txt
@@ -116,7 +116,7 @@
# logo to use
$logo = "git-logo.png";
-# the ‘favicon’
+# the favicon
$favicon = "git-favicon.png";
----
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index 1236077..e232c0c 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -90,7 +90,7 @@
Called whenever a change has been abandoned.
====
- change-abandoned --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --abandoner <abandoner> --reason <reason>
+ change-abandoned --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --abandoner <abandoner> --commit <sha1> --reason <reason>
====
change-restored
@@ -99,7 +99,7 @@
Called whenever a change has been restored.
====
- change-restored --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --restorer <restorer> --reason <reason>
+ change-restored --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --restorer <restorer> --commit <sha1> --reason <reason>
====
ref-updated
@@ -120,6 +120,15 @@
reviewer-added --change <change id> --change-url <change url> --project <project name> --branch <branch> --reviewer <reviewer>
====
+topic-changed
+~~~~~~~~~~~~~
+
+Called whenever a change's topic is changed from the Web UI or via the REST API.
+
+====
+ topic-changed --change <change id> --changer <changer> --old-topic <old topic> --new-topic <new topic>
+====
+
cla-signed
~~~~~~~~~~
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 1ff7e24..c3e09c4 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -177,7 +177,7 @@
[[label_abbreviation]]
`label.Label-Name.abbreviation`
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An abbreviated name for a label shown as a compact column header, for
example on project dashboards. Defaults to all the uppercase characters
@@ -203,6 +203,12 @@
must be at least one positive value, or else submit will never be
enabled. To permit blocking submits, ensure a negative value is defined.
+* `AnyWithBlock`
++
+The lowest possible negative value, if present, blocks a submit, Any
+other value enables a submit. To permit blocking submits, ensure
+that a negative value is defined.
+
* `MaxNoBlock`
+
The highest possible positive value is required to enable submit, but
@@ -238,6 +244,30 @@
configuration for this label in child projects will be ignored. Defaults
to true.
+[[label_branch]]
+`label.Label-Name.branch`
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+By default a given project's label applicable scope is all changes
+on all branches of this project and its child projects.
+
+Label's applicable scope can be branch specific via configuration.
+E.g. create a label `Video-Qualify` on parent project and configure
+the `branch` as:
+
+====
+ [label "Video-Qualify"]
+ branch = refs/heads/video-1.0/*
+ branch = refs/heads/video-1.1/Kino
+====
+
+Then *only* changes in above branch scope of parent project and child
+projects will be affected by `Video-Qualify`.
+
+NOTE: The `branch` is independent from the branch scope defined in `access`
+parts in `project.config` file. That means from the UI a user can always
+assign permissions for that label on a branch, but this permission is then
+ignored if the label doesn't apply for that branch.
[[label_example]]
Example
diff --git a/Documentation/config-login-register.txt b/Documentation/config-login-register.txt
index d0e0fc5..867f0d4 100644
--- a/Documentation/config-login-register.txt
+++ b/Documentation/config-login-register.txt
@@ -135,4 +135,9 @@
git clone ssh://user@localhost:29418/REPOSITORY_NAME.git
user@host:~$
-----
\ No newline at end of file
+----
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 8de8e59..3b8bffa 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -65,6 +65,13 @@
related to a user editing the commit message through the Gerrit UI. It is a
`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
+Footer.vm
+~~~~~~~~~
+
+The `Footer.vm` template will determine the contents of the footer text
+appended to the end of all outgoing emails after the ChangeFooter and
+CommentFooter.
+
Merged.vm
~~~~~~~~~
@@ -83,15 +90,10 @@
~~~~~~~~~~~~
The `NewChange.vm` template will determine the contents of the email related
-to a user submitting a new change for review. It is a `ChangeEmail`: see
-`ChangeSubject.vm` and `ChangeFooter.vm`.
-
-RebasedPatchSet.vm
-~~~~~~~~~~~~~~~~~~
-
-The `RebasedPatchSet.vm` template will determine the contents of the email
-related to a user rebasing a patchset for a change through the Gerrit UI.
-It is a `ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
+to a user submitting a new change for review. This includes changes created
+by actions made by the user in the Web UI such as cherry picking a commit or
+reverting a change. It is a `ChangeEmail`: see `ChangeSubject.vm` and
+`ChangeFooter.vm`.
RegisterNewEmail.vm
~~~~~~~~~~~~~~~~~~~
@@ -103,7 +105,9 @@
~~~~~~~~~~~~~~~~~~
The `ReplacePatchSet.vm` template will determine the contents of the email
-related to a user submitting a new patchset for a change. It is a
+related to a user submitting a new patchset for a change. This includes
+patchsets created by actions made by the user in the Web UI such as editing
+the commit message, cherry picking a commit, or rebasing a change. It is a
`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
Restored.vm
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index ab5d04a..1b09d19 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -2,18 +2,39 @@
======================================
Gerrit supports link:dev-plugins.html[plugin-based] validation of
-uploaded commits.
+commits.
-This allows plugins to perform additional validation checks against
-uploaded commits, and send back a warning or error message to the git
-client.
+[[new-commit-validation]]
+New commit validation
+---------------------
-To make use of this feature, a plugin must implement the `CommitValidationListener`
-interface.
+
+Plugins implementing the `CommitValidationListener` interface can
+perform additional validation checks against new commits.
+
+If the commit fails the validation, the plugin can either provide a
+message that will be sent back to the git client, or throw an exception
+which will cause the commit to be rejected.
+
+Validation applies to both commits uploaded via `git push`, and new
+commits generated via Gerrit's Web UI features such as the rebase, revert
+and cherry-pick buttons.
Out of the box, Gerrit includes a plugin that checks the length of the
subject and body lines of commit messages on uploaded commits.
+[[pre-merge-validation]]
+Pre-merge validation
+--------------------
+
+
+Plugins implementing the `MergeValidationListener` interface can
+perform additional validation checks against commits before they
+are merged to the git repository.
+
+If the commit fails the validation, the plugin can throw an exception
+which will cause the merge to fail.
+
GERRIT
------
diff --git a/Documentation/config.defs b/Documentation/config.defs
new file mode 100644
index 0000000..28dd2c8
--- /dev/null
+++ b/Documentation/config.defs
@@ -0,0 +1,19 @@
+DOCUMENTATION_DEPS = {
+ "install-quick.txt": ["config-login-register.txt"],
+ "install.txt": ["database-setup.txt"],
+}
+
+def documentation_attributes(revision):
+ return [
+ 'toc',
+ 'newline="\\n"',
+ 'asterisk="*"',
+ 'plus="+"',
+ 'caret="^"',
+ 'startsb="["',
+ 'endsb="]"',
+ 'tilde="~"',
+ 'source-highlighter=prettify',
+ 'stylesheet=doc.css',
+ 'revnumber="%s"' % revision,
+ ]
diff --git a/Documentation/database-setup.txt b/Documentation/database-setup.txt
index c559b0e..3800473 100644
--- a/Documentation/database-setup.txt
+++ b/Documentation/database-setup.txt
@@ -63,3 +63,53 @@
Visit MySQL's link:http://dev.mysql.com/doc/[documentation] for further
information regarding using MySQL.
+
+[[createdb_oracle]]
+Oracle
+~~~~~~
+
+PostgreSQL or H2 is the recommended database for Gerrit Code Review.
+Oracle is supported for environments where running on an existing Oracle
+installation simplifies administrative overheads, such as database backups.
+
+Create a user for the web application within sqlplus, assign it a
+password, and grant the user full rights on the newly created database:
+
+----
+ SQL> create user gerrit2 identified by secret_password default tablespace users;
+ SQL> grant connect, resources to gerrit2;
+----
+
+JDBC driver ojdbc6.jar must be obtained from your Oracle distribution. Gerrit
+initialization process tries to copy it from a known location:
+
+----
+/u01/app/oracle/product/11.2.0/xe/jdbc/lib/ojdbc6.jar
+----
+
+If this file can not be located at this place, then the alternative location
+can be provided.
+
+Instance name is the Oracle SID. Sample database section in
+$site_path/etc/gerrit.config:
+
+----
+[database]
+ type = oracle
+ instance = xe
+ hostname = localhost
+ username = gerrit2
+ port = 1521
+----
+
+Sample database section in $site_path/etc/secure.config:
+
+----
+[database]
+ password = secret_pasword
+----
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
new file mode 100644
index 0000000..74f56b3
--- /dev/null
+++ b/Documentation/dev-buck.txt
@@ -0,0 +1,340 @@
+Gerrit Code Review - Building with Buck
+=======================================
+
+
+Installation
+------------
+
+There is currently no binary distribution of Buck, so it has to be manually
+built and installed. Apache Ant is required. Currently only Linux and Mac
+OS are supported.
+
+Clone the git and build it:
+
+----
+ git clone https://gerrit.googlesource.com/buck
+ cd buck
+ ant
+----
+
+Make sure you have a `bin/` directory in your home directory and that
+it is included in your path:
+
+----
+ mkdir ~/bin
+ PATH=~/bin:$PATH
+----
+
+Add a symbolic link in `~/bin` to the buck executable:
+
+----
+ ln -s `pwd`/bin/buck ~/bin/
+----
+
+Verify that `buck` is accessible:
+
+----
+ which buck
+----
+
+If you plan to use the link:#buck-daemon[Buck daemon] add a symbolic
+link in `~/bin` to the buckd executable:
+
+----
+ ln -s `pwd`/bin/buckd ~/bin/
+----
+
+
+[[eclipse]]
+Eclipse Integration
+-------------------
+
+
+Generating the Eclipse Project
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create the Eclipse project by building the `eclipse` target:
+
+----
+ buck build eclipse
+----
+
+In Eclipse, choose 'Import existing project' and select the `gerrit` project
+from the current working directory.
+
+Expand the `gerrit` project, right-click on the `buck-out` folder, select
+'Properties', and then under 'Attributes' check 'Derived'.
+
+Note that if you make any changes in the project configuration that get
+saved to the `.project` file, for example adding Resource Filters on a
+folder, they will be overwritten the next time you run `buck build eclipse`.
+
+
+Refreshing the Classpath
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Normally `buck build eclipse` does everything necessary to generate a working Eclipse
+environment, but if the code doesn't compile and an updated classpath is needed, the
+Eclipse project can be refreshed and missing dependency JARs can be downloaded by
+building the `eclipse_project` and `download` targets:
+
+----
+ buck build eclipse_project download
+----
+
+
+Attaching Sources
+~~~~~~~~~~~~~~~~~
+
+To save time and bandwidth source JARs are only downloaded by the buck
+build where necessary to compile Java source into JavaScript using the
+GWT compiler. Additional sources may be obtained, allowing Eclipse to
+show documentation or dive into the implementation of a library JAR:
+
+----
+ buck build download_sources
+----
+
+
+[[build]]
+Building on the Command Line
+----------------------------
+
+
+Gerrit Development WAR File
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To build the Gerrit web application:
+
+----
+ buck build gerrit
+----
+
+The output executable WAR will be placed in:
+
+----
+ buck-out/gen/gerrit.war
+----
+
+
+Extension and Plugin API JAR Files
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To build the extension and plugin API JAR files:
+
+----
+ buck build api
+----
+
+The output JAR files will be placed in:
+
+----
+ buck-out/gen/{extension,plugin}-api.jar
+----
+
+Install {extension,plugin}-api to the local maven repository:
+
+----
+ buck build api_install
+----
+
+Deploy {extension,plugin}-api to the remote maven repository
+
+----
+ buck build api_deploy
+----
+
+The type of the repo is induced from the Gerrit version name, i.e.
+* 2.8-SNAPSHOT: shapshot repo
+* 2.8: release repo
+
+Plugins
+~~~~~~~
+
+To build all core plugins:
+
+----
+ buck build plugins:core
+----
+
+The output JAR files for individual plugins will be placed in:
+
+----
+ buck-out/gen/plugins/<name>/<name>.jar
+----
+
+The JAR files will also be packaged in:
+
+----
+ buck-out/gen/plugins/core.zip
+----
+
+To build a specific plugin:
+
+----
+ buck build plugins/<name>
+----
+
+The output JAR file will be be placed in:
+
+----
+ buck-out/gen/plugins/<name>/<name>.jar
+----
+
+Note that when building an individual plugin, the `core.zip` package
+is not regenerated.
+
+
+[[documentation]]
+Documentation
+~~~~~~~~~~~~~
+
+To build the documentation:
+
+----
+ buck build docs
+----
+
+The generated html files will be placed in:
+
+----
+ buck-out/gen/Documentation
+----
+
+The html files will also be bundled into `html.zip` in the same location.
+
+
+[[release]]
+Gerrit Release WAR File
+~~~~~~~~~~~~~~~~~~~~~~~
+
+To build the release of the Gerrit web application, including documentation and
+all core plugins:
+
+----
+ buck build release
+----
+
+The output release WAR will be placed in:
+
+----
+ buck-out/gen/release.war
+----
+
+[[tests]]
+Running Unit Tests
+------------------
+
+To run all tests including acceptance tests:
+
+----
+ buck test --all
+----
+
+To exclude slow tests:
+
+----
+ buck test --all --exclude slow
+----
+
+To run a specific test, e.g. the acceptance test
+`com.google.gerrit.acceptance.git.HttpPushForReviewIT`:
+
+----
+ buck test //gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:HttpPushForReviewIT
+----
+
+
+Dependencies
+------------
+
+Dependency JARs are normally downloaded automatically, but Buck can inspect
+its graph and download any missing JAR files. This is useful to enable
+subsequent builds to run without network access.
+
+Force a download of dependency JARs by building the `download` target:
+
+----
+ buck build download
+----
+
+When downloading from behind a proxy (which is common in some corporate
+environments), it might be necessary to explicitly specify the proxy that
+is then used by `curl`:
+
+----
+ export http_proxy=http://<proxy_user_id>:<proxy_password>@<proxy_server>:<proxy_port>
+----
+
+Redirection to local mirrors of Maven Central and the Gerrit storage
+bucket is supported by defining specific properties in
+`local.properties`, a file that is not tracked by Git:
+
+----
+ echo download.GERRIT = http://nexus.my-company.com/ >>local.properties
+ echo download.MAVEN_CENTRAL = http://nexus.my-company.com/ >>local.properties
+----
+
+The `local.properties` file may be placed in the root of the gerrit repository
+being built, or in `~/.gerritcodereview/`. The file in the root of the gerrit
+repository has precedence.
+
+Building against unpublished Maven JARs
+---------------------------------------
+
+To build against unpublished Maven JARs, like gwtorm or PrologCafe, the custom
+JARs must be installed in the local Maven repository (mvn clean install) and
+maven_jar() must be updated to point to the MAVEN_LOCAL Maven repository for
+that artifact:
+
+----
+ maven_jar(
+ name = 'gwtorm',
+ id = 'gwtorm:gwtorm:42',
+ license = 'Apache2.0',
+ repository = MAVEN_LOCAL,
+ )
+----
+
+Caching Build Results
+~~~~~~~~~~~~~~~~~~~~~
+
+Build results can be locally cached, saving rebuild time when
+switching between Git branches. Buck's documentation covers
+caching in link:http://facebook.github.io/buck/concept/buckconfig.html[buckconfig].
+The trivial case using a local directory is:
+
+----
+ cat >.buckconfig.local <<EOF
+ [cache]
+ mode = dir
+ dir = buck-cache
+ EOF
+----
+
+[[buck-daemon]]
+Using Buck daemon
+~~~~~~~~~~~~~~~~~
+
+Buck ships with daemon command `buckd`, which uses
+link:https://github.com/martylamb/nailgun[Nailgun] protocol for running
+Java programs from the command line without incurring the JVM startup
+overhead. Using a Buck daemon can save significant amounts of time as it
+avoids the overhead of starting a Java virtual machine, loading the buck class
+files and parsing the build files for each command. It is safe to run several
+buck daemons started from different project directories and they will not
+interfere with each other. Buck's documentation covers daemon in
+http://facebook.github.io/buck/command/buckd.html[buckd]. The trivial case is
+to run `buckd` from the project's root directory and use `buck` as usually:
+
+----
+$>buckd
+$>buck build gerrit
+Using buckd.
+[-] PARSING BUILD FILES...FINISHED 0.6s
+[-] BUILDING...FINISHED 0.2s
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 84cb1e0..edc072d 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -94,13 +94,22 @@
====
The Change-Id is, as usual, created by a local git hook. To install it, simply
-copy one from the checkout and make it executable:
+copy it from the checkout and make it executable:
====
cp ./gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg .git/hooks/
chmod +x .git/hooks/commit-msg
====
+If you are working on core plugins, you will also need to install the
+same hook in the submodules:
+
+====
+ export hook=$(pwd)/.git/hooks/commit-msg
+ git submodule foreach 'cp -p "$hook" "$(git rev-parse --git-dir)/hooks/"'
+====
+
+
To set up git's remote for easy pushing, run the following:
====
@@ -127,7 +136,7 @@
in the formatting guidelines. This is especially true within the
same file.
* Review your change in Gerrit to see if it highlights
- mistakingly deleted/added spaces on lines, trailing spaces.
+ mistakenly deleted/added spaces on lines, trailing spaces.
* Line length should be 80 or less, unless the code reads
better with something slightly longer. Shorter lines not only
help reviewers who may use a tablet to review the code, but future
@@ -271,6 +280,10 @@
Developers concerned with stable branches are encouraged to backport or push
patchsets to these branches, even if no new release is planned.
+Fixes that are known to be needed for a particular release should be pushed
+for review on that release's stable branch. It will then be included in
+the master branch when the stable branch is merged back.
+
GERRIT
------
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt
index 2e43b96..3cb58b1 100644
--- a/Documentation/dev-design.txt
+++ b/Documentation/dev-design.txt
@@ -66,7 +66,7 @@
Gerrit 2.x is a complete rewrite of the Gerrit fork, completely
changing the implementation from Python on Google App Engine, to Java
-on a J2EE servlet container and a SQL database.
+on a J2EE servlet container and an SQL database.
* link:http://video.google.com/videoplay?docid=-8502904076440714866[Mondrian Code Review On The Web]
* link:http://code.google.com/p/rietveld/[Rietveld - Code Review for Subversion]
@@ -445,7 +445,7 @@
-------
Gerrit targets for sub-250 ms per page request, mostly by using
-very compact JSON payloads bewteen client and server. However, as
+very compact JSON payloads between client and server. However, as
most of the serving stack (network, hardware, metadata
database) is out of control of the Gerrit developers, no real
guarantees can be made about latency.
@@ -474,7 +474,7 @@
Out of the box, Gerrit will handle the "Default Maximum". Site
administrators may reconfigure their servers by editing gerrit.config
to run closer to the estimated maximum if sufficient memory is made
-avaliable to the JVM and the relevant cache.*.memoryLimit variables
+available to the JVM and the relevant cache.*.memoryLimit variables
are increased from their defaults.
Discussion
@@ -671,7 +671,7 @@
PostgreSQL and MySQL can be configured to replicate their data to
other systems, where they are applied to a warm-standby backup in
-real time. Gerrit instances which care about reduduncy will setup
+real time. Gerrit instances which care about redundancy will setup
this feature of PostgreSQL or MySQL to ensure the warm-standby is
reasonably current should the master go offline.
@@ -699,7 +699,7 @@
Changes submitted and merged into a branch also update the
Git reflog. These logs are available only to the Gerrit site
administrator, and they are not replicated through the automatic
-replication noted earlier. These logs are primarly recorded for an
+replication noted earlier. These logs are primarily recorded for an
"oh s**t" moment where the administrator has to rewind data. In most
installations they are a waste of disk space. Future versions of
JGit may allow disabling these logs, and Gerrit may take advantage
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 019c78f..d2fc8f0 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -8,19 +8,6 @@
runtime debugging environment.
-[[maven]]
-Maven Plugin
-------------
-
-Install the Maven Integration plugins.
-
-In Eclipse version 3.7 (Indigo) and later, these are available in the
-default update site and can be found under the 'Collaboration' category.
-
-For older versions the update site must be manually added; the link can
-be found on the http://www.eclipse.org/m2e/download/[m2eclipse download page].
-
-
[[Formatting]]
Code Formatter Settings
-----------------------
@@ -32,23 +19,12 @@
settings prefer when formatting source code.
-Import Projects
----------------
-
-Import the projects into Eclipse by going to File -> Import... -> Maven ->
-Existing Maven Projects and selecting the directory containing pom.xml.
-
-Some of the source code is generated with ANTLR sources. To build
-these files, right click on the imported projects, Maven -> Update
-Project Configuration. This will resolve compile errors identified
-after import.
-
-
Site Initialization
-------------------
-link:dev-readme.html#build[Build] once on the command line and
-then follow link:dev-readme.html#init[Site Initialization] in the
+Build once on the command line with
+link:dev-buck.html#build[Buck] and then follow
+link:dev-readme.html#init[Site Initialization] in the
Developer Setup guide to configure a local site for testing.
@@ -58,10 +34,10 @@
Running the Daemon
~~~~~~~~~~~~~~~~~~
-Duplicate the existing `pgm_daemon` launch configuration:
+Duplicate the existing launch configuration:
* Run -> Debug Configurations ...
-* Java Application -> `pgm_daemon`
+* Java Application -> `buck_daemon_ui_*`
* Right click, Duplicate
* Modify the name to be unique.
@@ -73,25 +49,16 @@
* Switch to Common tab.
* Change Save as to be Local file.
+* Close the Debug Configurations dialog and save the changes when prompted.
-[[hosted-mode]]
Running Hosted Mode
~~~~~~~~~~~~~~~~~~~
-To debug the GWT code executing in the web browser, two additional Git
-repositories need to be cloned.
-
-* https://gerrit.googlesource.com/gwtjsonrpc
-* https://gerrit.googlesource.com/gwtorm
-
-In Eclipse, import the pom.xml file in the root directory of each of
-these cloned gits via General -> Maven Projects.
-
-Duplicate the existing `gwtui_dbg` launch configuration:
+Duplicate the existing launch configuration:
* Run -> Debug Configurations ...
-* Java Application -> `gwtui_dbg`
+* Java Application -> `buck_gwt_debug`
* Right click, Duplicate
* Modify the name to be unique.
@@ -103,23 +70,22 @@
* Switch to Common tab.
* Change Save as to be Local file.
+* Close the Debug Configurations dialog and save the changes when prompted.
[[known-problems]]
Known problems
--------------
-* When running Gerrit under the Eclipse debugger, code that attempts
-to load Prolog code may erroneously raise ClassNotFoundException,
-claiming that classes in the `Gerrit` package can't be found. The
-error can often be resolved by rebuilding Gerrit with `mvn package`
-and restarting the debug session.
-
* OpenID authentication won't work in hosted mode, so you need to change
the link:config-gerrit.html#auth.type[auth.type] configuration parameter
to `DEVELOPMENT_BECOME_ANY_ACCOUNT` to disable OpenID and allow you to
impersonate whatever account you otherwise would've used.
+* Error "Cannot create ReviewDb" occurs if the test site is already running.
+Stop the test site with `gerrit.sh stop` before attempting to run hosted mode
+debugging.
+
* Gerrit site doesn't appear, only directory listing is shown. Web toolkit
developer browser plugin is missing. If there is no warning, that browser
plugin is missing with the suggestion to install it, you can install the
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 12838fe..0ba51c4 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -49,14 +49,13 @@
ask again for all properties including those with predefined default
values.
-. clone the sample helloworld plugin:
+. clone the sample plugin:
+
-This is a Maven project that adds an SSH command to Gerrit to print
-out a hello world message. It can be taken as an example to develop
-an own plugin.
+This is a project that demonstrates the various features of the
+plugin API. It can be taken as an example to develop an own plugin.
+
----
-$ git clone https://gerrit.googlesource.com/plugins/helloworld
+$ git clone https://gerrit.googlesource.com/plugins/cookbook-plugin
----
+
When starting from this example one should take care to adapt the
@@ -64,7 +63,7 @@
the plugin is developed. If the plugin is developed for a released
Gerrit version (no `SNAPSHOT` version) then the URL for the
`gerrit-api-repository` in the `pom.xml` needs to be changed to
-`https://gerrit-api.commondatastorage.googleapis.com/release/`.
+`https://gerrit-api.storage.googleapis.com/release/`.
[[API]]
API
@@ -193,7 +192,7 @@
====
MyInitStep needs to follow the standard Gerrit InitStep syntax
-and behaviour: writing to the console using the injected ConsoleUI
+and behavior: writing to the console using the injected ConsoleUI
and accessing / changing configuration settings using Section.Factory.
In addition to the standard Gerrit init injections, plugins receive
@@ -216,7 +215,8 @@
Plugins InitStep cannot refer to Gerrit DB Schema or any other Gerrit runtime
objects injected at startup.
-====
+[source,java]
+----
public class MyInitStep implements InitStep {
private final ConsoleUI ui;
private final Section.Factory sections;
@@ -237,7 +237,7 @@
mySection.string("Link name", "linkname", "MyLink");
}
}
-====
+----
[[classpath]]
Classpath
@@ -264,44 +264,47 @@
Command implementations must extend the base class SshCommand:
-====
- import com.google.gerrit.sshd.SshCommand;
+[source,java]
+----
+import com.google.gerrit.sshd.SshCommand;
- class PrintHello extends SshCommand {
- protected abstract void run() {
- stdout.print("Hello\n");
- }
+class PrintHello extends SshCommand {
+ protected abstract void run() {
+ stdout.print("Hello\n");
}
-====
+}
+----
If no Guice modules are declared in the manifest, SSH commands may
use auto-registration by providing an `@Export` annotation:
-====
- import com.google.gerrit.extensions.annotations.Export;
- import com.google.gerrit.sshd.SshCommand;
+[source,java]
+----
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.sshd.SshCommand;
- @Export("print")
- class PrintHello extends SshCommand {
- protected abstract void run() {
- stdout.print("Hello\n");
- }
+@Export("print")
+class PrintHello extends SshCommand {
+ protected abstract void run() {
+ stdout.print("Hello\n");
}
-====
+}
+----
If explicit registration is being used, a Guice module must be
supplied to register the SSH command and declared in the manifest
with the `Gerrit-SshModule` attribute:
-====
- import com.google.gerrit.sshd.PluginCommandModule;
+[source,java]
+----
+import com.google.gerrit.sshd.PluginCommandModule;
- class MyCommands extends PluginCommandModule {
- protected void configureCommands() {
- command("print").to(PrintHello.class);
- }
+class MyCommands extends PluginCommandModule {
+ protected void configureCommands() {
+ command("print").to(PrintHello.class);
}
-====
+}
+----
For a plugin installed as name `helloworld`, the command implemented
by PrintHello class will be available to users as:
@@ -310,6 +313,284 @@
$ ssh -p 29418 review.example.com helloworld print
----
+[[capabilities]]
+Plugin Owned Capabilities
+-------------------------
+
+Plugins may provide their own capabilities and restrict usage of SSH
+commands to the users who are granted those capabilities.
+
+Plugins define the capabilities by overriding the `CapabilityDefinition`
+abstract class:
+
+[source,java]
+----
+public class PrintHelloCapability extends CapabilityDefinition {
+ @Override
+ public String getDescription() {
+ return "Print Hello";
+ }
+}
+----
+
+If no Guice modules are declared in the manifest, UI actions may
+use auto-registration by providing an `@Export` annotation:
+
+[source,java]
+----
+@Export("printHello")
+public class PrintHelloCapability extends CapabilityDefinition {
+ ...
+}
+----
+
+Otherwise the capability must be bound in a plugin module:
+
+[source,java]
+----
+public class HelloWorldModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(CapabilityDefinition.class)
+ .annotatedWith(Exports.named("printHello"))
+ .to(PrintHelloCapability.class);
+ }
+}
+----
+
+With a plugin-owned capability defined in this way, it is possible to restrict
+usage of an SSH command or `UiAction` to members of the group that were granted
+this capability in the usual way, using the `RequiresCapability` annotation:
+
+[source,java]
+----
+@RequiresCapability("printHello")
+@CommandMetaData(name="print", description="Print greeting in different languages")
+public final class PrintHelloWorldCommand extends SshCommand {
+ ...
+}
+----
+
+Or with `UiAction`:
+
+[source,java]
+----
+@RequiresCapability("printHello")
+public class SayHelloAction extends UiAction<RevisionResource>
+ implements RestModifyView<RevisionResource, SayHelloAction.Input> {
+ ...
+}
+----
+
+Capability scope was introduced to differentiate between plugin-owned
+capabilities and core capabilities. Per default the scope of the
+`@RequiresCapability` annotation is `CapabilityScope.CONTEXT`, that means:
+
+* when `@RequiresCapability` is used within a plugin the scope of the
+capability is assumed to be that plugin.
+
+* If `@RequiresCapability` is used within the core Gerrit Code Review server
+(and thus is outside of a plugin) the scope is the core server and will use
+the `GlobalCapability` known to Gerrit Code Review server.
+
+If a plugin needs to use a core capability name (e.g. "administrateServer")
+this can be specified by setting `scope = CapabilityScope.CORE`:
+
+[source,java]
+----
+@RequiresCapability(value = "administrateServer", scope =
+ CapabilityScope.CORE)
+ ...
+----
+
+[[ui_extension]]
+UI Extension
+------------
+
+Plugins can contribute their own UI commands on core Gerrit pages.
+This is useful for workflow customization or exposing plugin functionality
+through the UI in addition to SSH commands and the REST API.
+
+For instance a plugin to integrate Jira with Gerrit changes may contribute its
+own "File bug" button to allow filing a bug from the change page or plugins to
+integrate continuous integration systems may contribute a "Schedule" button to
+allow a CI build to be scheduled manually from the patch set panel.
+
+Two different places on core Gerrit pages are currently supported:
+
+* Change screen
+* Project info screen
+
+Plugins contribute UI actions by implementing the `UiAction` interface:
+
+[source,java]
+----
+@RequiresCapability("printHello")
+class HelloWorldAction implements UiAction<RevisionResource>,
+ RestModifyView<RevisionResource, HelloWorldAction.Input> {
+ static class Input {
+ boolean french;
+ String message;
+ }
+
+ private Provider<CurrentUser> user;
+
+ @Inject
+ HelloWorldAction(Provider<CurrentUser> user) {
+ this.user = user;
+ }
+
+ @Override
+ public String apply(RevisionResource rev, Input input) {
+ final String greeting = input.french
+ ? "Bonjour"
+ : "Hello";
+ return String.format("%s %s from change %s, patch set %d!",
+ greeting,
+ Strings.isNullOrEmpty(input.message)
+ ? Objects.firstNonNull(user.get().getUserName(), "world")
+ : input.message,
+ rev.getChange().getId().toString(),
+ rev.getPatchSet().getPatchSetId());
+ }
+
+ @Override
+ public Description getDescription(
+ RevisionResource resource) {
+ return new Description()
+ .setLabel("Say hello")
+ .setTitle("Say hello in different languages");
+ }
+}
+----
+
+`UiAction` must be bound in a plugin module:
+
+[source,java]
+----
+public class Module extends AbstractModule {
+ @Override
+ protected void configure() {
+ install(new RestApiModule() {
+ @Override
+ protected void configure() {
+ post(REVISION_KIND, "say-hello")
+ .to(HelloWorldAction.class);
+ }
+ });
+ }
+}
+----
+
+The module above must be declared in pom.xml for Maven driven plugins:
+
+[source,xml]
+----
+<manifestEntries>
+ <Gerrit-Module>com.googlesource.gerrit.plugins.cookbook.Module</Gerrit-Module>
+</manifestEntries>
+----
+
+or in the BUCK configuration file for Buck driven plugins:
+
+[source,python]
+----
+manifest_entries = [
+ 'Gerrit-Module: com.googlesource.gerrit.plugins.cookbook.Module',
+]
+----
+
+In some use cases more user input must be gathered, for that `UiAction` can be
+combined with the JavaScript API. This would display a small popup near the
+activation button to gather additional input from the user. The JS file is
+typically put in the `static` folder within the plugin's directory:
+
+[source,javascript]
+----
+Gerrit.install(function(self) {
+ function onSayHello(c) {
+ var f = c.textfield();
+ var t = c.checkbox();
+ var b = c.button('Say hello', {onclick: function(){
+ c.call(
+ {message: f.value, french: t.checked},
+ function(r) {
+ c.hide();
+ window.alert(r);
+ c.refresh();
+ });
+ }});
+ c.popup(c.div(
+ c.prependLabel('Greeting message', f),
+ c.br(),
+ c.label(t, 'french'),
+ c.br(),
+ b));
+ f.focus();
+ }
+ self.onAction('revision', 'say-hello', onSayHello);
+});
+----
+
+The JS module must be exposed as a `WebUiPlugin` and bound as
+an HTTP Module:
+
+[source,java]
+----
+public class HttpModule extends HttpPluginModule {
+ @Override
+ protected void configureServlets() {
+ DynamicSet.bind(binder(), WebUiPlugin.class)
+ .toInstance(new JavaScriptPlugin("hello.js"));
+ }
+}
+----
+
+The HTTP module above must be declared in pom.xml for Maven driven plugins:
+
+[source,xml]
+----
+<manifestEntries>
+ <Gerrit-HttpModule>com.googlesource.gerrit.plugins.cookbook.HttpModule</Gerrit-HttpModule>
+</manifestEntries>
+----
+
+or in the BUCK configuration file for Buck driven plugins
+
+[source,python]
+----
+manifest_entries = [
+ 'Gerrit-HttpModule: com.googlesource.gerrit.plugins.cookbook.HttpModule',
+]
+----
+
+If `UiAction` is annotated with the `@RequiresCapability` annotation, then the
+capability check is done during the `UiAction` gathering, so the plugin author
+doesn't have to set `UiAction.Description.setVisible()` explicitly in this
+case.
+
+The following prerequisities must be met, to satisfy the capability check:
+
+* user is authenticated
+* user is a member of the Administrators group, or
+* user is a member of a group which has the required capability
+
+The `apply` method is called when the button is clicked. If `UiAction` is
+combined with JavaScript API (its own JavaScript function is provided),
+then a popup dialog is normally opened to gather additional user input.
+A new button is placed on the popup dialog to actually send the request.
+
+Every `UiAction` exposes a REST API endpoint. The endpoint from the example above
+can be accessed from any REST client, i. e.:
+
+====
+ curl -X POST -H "Content-Type: application/json" \
+ -d '{message: "François", french: true}' \
+ --digest --user joe:secret \
+ http://host:port/a/changes/1/revisions/1/cookbook~say-hello
+ "Bonjour François from change 1, patch set 1!"
+====
+
[[http]]
HTTP Servlets
-------------
@@ -319,38 +600,40 @@
Servlets may use auto-registration to declare the URL they handle:
-====
- import com.google.gerrit.extensions.annotations.Export;
- import com.google.inject.Singleton;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
+[source,java]
+----
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.inject.Singleton;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
- @Export("/print")
- @Singleton
- class HelloServlet extends HttpServlet {
- protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
- res.setContentType("text/plain");
- res.setCharacterEncoding("UTF-8");
- res.getWriter().write("Hello");
- }
+@Export("/print")
+@Singleton
+class HelloServlet extends HttpServlet {
+ protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
+ res.setContentType("text/plain");
+ res.setCharacterEncoding("UTF-8");
+ res.getWriter().write("Hello");
}
-====
+}
+----
The auto registration only works for standard servlet mappings like
`/foo` or `/foo/*`. Regex style bindings must use a Guice ServletModule
to register the HTTP servlets and declare it explicitly in the manifest
with the `Gerrit-HttpModule` attribute:
-====
- import com.google.inject.servlet.ServletModule;
+[source,java]
+----
+import com.google.inject.servlet.ServletModule;
- class MyWebUrls extends ServletModule {
- protected void configureServlets() {
- serve("/print").with(HelloServlet.class);
- }
+class MyWebUrls extends ServletModule {
+ protected void configureServlets() {
+ serve("/print").with(HelloServlet.class);
}
-====
+}
+----
For a plugin installed as name `helloworld`, the servlet implemented
by HelloServlet class will be available to users as:
@@ -369,12 +652,13 @@
Plugins can use this to store any data they want.
-====
- @Inject
- MyType(@PluginData java.io.File myDir) {
- new FileInputStream(new File(myDir, "my.config"));
- }
-====
+[source,java]
+----
+@Inject
+MyType(@PluginData java.io.File myDir) {
+ new FileInputStream(new File(myDir, "my.config"));
+}
+----
[[documentation]]
Documentation
@@ -384,11 +668,11 @@
`/Documentation/*` or `/static/*`, the core Gerrit server will
automatically export these resources over HTTP from the plugin JAR.
-Static resources under `static/` directory in the JAR will be
+Static resources under the `static/` directory in the JAR will be
available as `/plugins/helloworld/static/resource`. This prefix is
configurable by setting the `Gerrit-HttpStaticPrefix` attribute.
-Documentation files under `Documentation/` directory in the JAR
+Documentation files under the `Documentation/` directory in the JAR
will be available as `/plugins/helloworld/Documentation/resource`. This
prefix is configurable by setting the `Gerrit-HttpDocumentationPrefix`
attribute.
@@ -444,9 +728,21 @@
of the file, minus the `*.html` extension, as the link text. Any
hyphens in the file name will be replaced with spaces.
+If a discovered file is named `about.md` or `about.html`, its
+content will be inserted in an 'About' section at the top of the
+auto-generated index page. If both `about.md` and `about.html`
+exist, only the first discovered file will be used.
+
If a discovered file name beings with `cmd-` it will be clustered
-into a 'Commands' section of the generated index page. All other
-files are clustered under a 'Documentation' section.
+into a 'Commands' section of the generated index page.
+
+If a discovered file name beings with `servlet-` it will be clustered
+into a 'Servlets' section of the generated index page.
+
+If a discovered file name beings with `rest-api-` it will be clustered
+into a 'REST APIs' section of the generated index page.
+
+All other files are clustered under a 'Documentation' section.
Some optional information from the manifest is extracted and
displayed as part of the index page, if present in the manifest:
@@ -481,6 +777,12 @@
Disabled plugins can be re-enabled using the
link:cmd-plugin-enable.html[plugin enable] command.
+SEE ALSO
+--------
+
+* link:js-api.html[JavaScript API]
+* link:dev-rest-api.html[REST API Developers' Notes]
+
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index 67b4aad..ced8648 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -1,12 +1,13 @@
Gerrit Code Review - Developer Setup
====================================
-Apache Maven is needed to compile the code, and a SQL database
-to house the review metadata. H2 is recommended for development
+Facebook Buck is needed to compile the code, and an SQL database to
+house the review metadata. H2 is recommended for development
databases, as it requires no external server process.
-Get the Source
---------------
+
+Getting the Source
+------------------
Create a new client workspace:
@@ -19,46 +20,27 @@
the core plugins, which are included as git submodules, are also
cloned.
+
+Compiling
+---------
+
+For details on how to build the source code with Buck, refer to:
+link:dev-buck.html#build[Building on the command line with Buck].
+
+
Configuring Eclipse
-------------------
To use the Eclipse IDE for development, please see
-link:dev-eclipse.html[Eclipse Setup] for more details on how to
-configure the workspace with the Maven build scripts.
+link:dev-eclipse.html[Eclipse Setup].
+For details on how to configure the Eclipse workspace with Buck,
+refer to: link:dev-buck.html#eclipse[Eclipse integration with Buck].
-[[build]]
-Building
---------
-
-From the command line:
-
-----
- mvn clean package
-----
-
-By default the build will run tests and build the documentation.
-
-To build without tests:
-
-----
- mvn clean package -DskipTests
-----
-
-To build without documentation:
-
-----
- mvn clean package -Dgerrit.documentation.skip
-----
-
-Output executable WAR will be placed in:
-
-----
- gerrit-war/target/gerrit-*.war
-----
Mac OS X
-~~~~~~~~
+--------
+
On Mac OS X ensure "Java For Mac OS X 10.5 Upate 4" (or later) has
been installed, and that `JAVA_HOME` is set to
"/System/Library/Frameworks/JavaVM.framework/Versions/1.6/Home".
@@ -75,7 +57,7 @@
testing site for development use:
----
- java -jar gerrit-war/target/gerrit-*.war init -d ../test_site
+ java -jar buck-out/gen/gerrit.war init -d ../test_site
----
Accept defaults by pressing Enter until 'init' completes, or add
@@ -97,7 +79,8 @@
Testing
-------
-[[run-acceptance-tests]]
+
+[[tests]]
Running the Acceptance Tests
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -108,15 +91,10 @@
started on that site. When the test has finished the Gerrit daemon is
shutdown.
-Since the acceptance tests are too expensive to run every time
-Gerrit is built, they are only executed during the Maven verify phase
-if the Maven profile `acceptance` is enabled.
+For instructions on running the integration tests with Buck,
+please refer to:
+link:dev-buck.html#tests[Running integration tests with Buck].
-To execute the acceptance tests run:
-
-----
- mvn clean verify -Pacceptance
-----
Running the Daemon
~~~~~~~~~~~~~~~~~~
@@ -125,7 +103,7 @@
copying to the test site:
----
- java -jar gerrit-war/target/gerrit-*.war daemon -d ../test_site
+ java -jar buck-out/gen/gerrit.war daemon -d ../test_site
----
@@ -136,7 +114,7 @@
command line. If the daemon is not currently running:
----
- java -jar gerrit-war/target/gerrit-*.war gsql -d ../test_site
+ java -jar buck-out/gen/gerrit.war gsql -d ../test_site
----
Or, if it is running and the database is in use, connect over SSH
@@ -213,11 +191,6 @@
* http://code.google.com/webtoolkit/download.html[Download]
-Apache Maven:
-
-* http://maven.apache.org/download.html[Download]
-* http://maven.apache.org/run-maven/index.html[Running]
-
Apache SSHD:
* http://mina.apache.org/sshd/[SSHD]
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index ffdb6ea..9c98fff 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -73,7 +73,7 @@
<repository>
<id>gerrit-maven-repository</id>
<name>Gerrit Maven Repository</name>
- <url>s3://gerrit-maven@commondatastorage.googleapis.com</url>
+ <url>gs://gerrit-maven</url>
<uniqueVersion>true</uniqueVersion>
</repository>
</distributionManagement>
@@ -86,9 +86,9 @@
<build>
<extensions>
<extension>
- <groupId>net.anzix.aws</groupId>
- <artifactId>s3-maven-wagon</artifactId>
- <version>3.2</version>
+ <groupId>com.googlesource.gerrit</groupId>
+ <artifactId>gs-maven-wagon</artifactId>
+ <version>3.3</version>
</extension>
</extensions>
</build>
@@ -107,7 +107,7 @@
<repository>
<id>gerrit-plugins-repository</id>
<name>Gerrit Plugins Repository</name>
- <url>s3://gerrit-plugins@commondatastorage.googleapis.com</url>
+ <url>gs://gerrit-plugins</url>
<uniqueVersion>true</uniqueVersion>
</repository>
</distributionManagement>
@@ -120,9 +120,9 @@
<build>
<extensions>
<extension>
- <groupId>net.anzix.aws</groupId>
- <artifactId>s3-maven-wagon</artifactId>
- <version>3.2</version>
+ <groupId>com.googlesource.gerrit</groupId>
+ <artifactId>gs-maven-wagon</artifactId>
+ <version>3.3</version>
</extension>
</extensions>
</build>
diff --git a/Documentation/dev-release-subproject.txt b/Documentation/dev-release-subproject.txt
index 5e3770d..956bd29 100644
--- a/Documentation/dev-release-subproject.txt
+++ b/Documentation/dev-release-subproject.txt
@@ -13,9 +13,9 @@
`gerrit-api-repository` in the `pom.xml` is correct.
+
If `Gerrit-ApiVersion` references a released Gerrit version it must be
-`https://gerrit-api.commondatastorage.googleapis.com/release/`, if
+`https://gerrit-api.stoarge.googleapis.com/release/`, if
`Gerrit-ApiVersion` references a snapshot Gerrit version it must be
-`https://gerrit-api.commondatastorage.googleapis.com/snapshot/`.
+`https://gerrit-api.storage.googleapis.com/snapshot/`.
* Build the latest snapshot and install it into the local Maven
repository:
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index cd7cd34..f9d0d0e 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -83,7 +83,7 @@
Create the Actual Release
----------------------------
+-------------------------
To create a Gerrit release the following steps have to be done:
@@ -157,7 +157,7 @@
link:dev-release-deploy-config.html#deploy-configuration-settings-xml[
configuration needed for deployment]
-* Push the Jars to `commondatastorage.googleapis.com`:
+* Push the Jars to `storage.googleapis.com`:
+
----
./tools/deploy_api.sh
diff --git a/Documentation/dev-rest-api.txt b/Documentation/dev-rest-api.txt
new file mode 100644
index 0000000..0175a62
--- /dev/null
+++ b/Documentation/dev-rest-api.txt
@@ -0,0 +1,89 @@
+Gerrit Code Review - REST API Developers' Notes
+===============================================
+
+This document is about developing the REST API. For details of the
+actual APIs available in Gerrit, please see the
+link:rest-api.html[REST API interface reference].
+
+
+Testing REST API Functionality
+------------------------------
+
+
+Basic Testing
+~~~~~~~~~~~~~
+
+Basic testing of REST API functionality can be done with `curl`:
+
+----
+ curl http://localhost:8080/path/to/api/
+----
+
+By default, `curl` sends `GET` requests. To test APIs with `PUT`, `POST`,
+or `DELETE`, an additional argument is required:
+
+----
+ curl -X PUT http://localhost:8080/path/to/api/
+ curl -X POST http://localhost:8080/path/to/api/
+ curl -X DELETE http://localhost:8080/path/to/api/
+----
+
+
+Sending Data in the Request
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Some REST APIs accept data in the request body of `PUT` and `POST` requests.
+
+Test data can be included from a local file:
+
+----
+ curl -X PUT -d@testdata.txt --header "Content-Type: application/json" http://localhost:8080/path/to/api/
+----
+
+Note that the `-d` option will remove the newlines from the content of the
+local file. If the content should be sent as-is then use the `--data-binary`
+option instead:
+
+----
+ curl -X PUT --data-binary @testdata.txt --header "Content-Type: text/plain" http://localhost:8080/path/to/api/
+----
+
+
+Authentication
+~~~~~~~~~~~~~~
+
+To test APIs that require authentication, the username and password must be specified on
+the command line:
+
+----
+ curl --digest --user username:password http://localhost:8080/a/path/to/api/
+----
+
+This makes it easy to switch users for testing of permissions.
+
+It is also possible to test with a username and password from the `.netrc`
+file (on Windows, `_netrc`):
+
+----
+ curl --digest -n http://localhost:8080/a/path/to/api/
+----
+
+In both cases, the password should be the user's link:user-upload.html#http[HTTP password].
+
+
+Verifying Header Content
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+To verify the headers returned from a REST API call, use `curl` in verbose mode:
+
+----
+ curl -v -n --digest -X DELETE http://localhost:8080/a/path/to/api/
+----
+
+The headers on both the request and the response will be printed.
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
diff --git a/Documentation/doc.css b/Documentation/doc.css
new file mode 100644
index 0000000..3e226fe
--- /dev/null
+++ b/Documentation/doc.css
@@ -0,0 +1,37 @@
+body {
+ margin: 1em;
+}
+
+#toctitle {
+ margin-top: 0.5em;
+ font-weight: bold;
+}
+
+h1, h2, h3, h4, h5, h6, #toctitle {
+ color: #527bbd;
+ font-family: sans-serif;
+}
+
+h1, h2, h3 {
+ border-bottom: 2px solid silver;
+}
+
+p {
+ margin: 0.5em 0 0.5em 0;
+}
+li p {
+ margin: 0.2em 0 0.2em 0;
+}
+
+pre {
+ border: 2px solid silver;
+ background: #ebebeb;
+ margin-left: 2em;
+ width: 100em;
+ color: darkgreen;
+ padding: 2px;
+}
+
+dl dt {
+ margin-top: 1em;
+}
diff --git a/Documentation/error-has-duplicates.txt b/Documentation/error-has-duplicates.txt
index e9e42f4..b5175c0 100644
--- a/Documentation/error-has-duplicates.txt
+++ b/Documentation/error-has-duplicates.txt
@@ -1,5 +1,5 @@
-... has duplicates
-==================
+\... has duplicates
+===================
With this error message Gerrit rejects to push a commit if its commit
message contains a Change-ID for which multiple changes can be found
diff --git a/Documentation/error-missing-changeid.txt b/Documentation/error-missing-changeid.txt
index b13f3b4..edbc63b 100644
--- a/Documentation/error-missing-changeid.txt
+++ b/Documentation/error-missing-changeid.txt
@@ -6,11 +6,12 @@
message if the commit message of the pushed commit does not contain
a Change-Id in the footer (the last paragraph).
-This error may happen for two reasons:
+This error may happen for different reasons:
. missing Change-Id in the commit message
. Change-Id is contained in the commit message but not in the last
paragraph
+. Change-Id is the only line in the commit message
You can see the commit messages for existing commits in the history
by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log].
@@ -51,6 +52,20 @@
Change-ID into the last paragraph. How to update the commit message
is explained link:error-push-fails-due-to-commit-message.html[here].
+Change-Id is the only line in the commit message
+------------------------------------------------
+
+Gerrit does not parse the subject of a commit message for the
+Change-Id even if this is the only and last paragraph of the commit
+message.
+
+If the Change-Id is the only line in the commit message you must update
+the commit message and insert a subject as the first line in the commit
+message. The Change-Id must be in the last paragraph of the commit
+message, i.e. separated from the subject by a blank line. How to update
+the commit message is explained
+link:error-push-fails-due-to-commit-message.html[here].
+
GERRIT
------
diff --git a/Documentation/gen_licenses.py b/Documentation/gen_licenses.py
new file mode 100755
index 0000000..fb03526
--- /dev/null
+++ b/Documentation/gen_licenses.py
@@ -0,0 +1,131 @@
+#!/usr/bin/python
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# TODO(sop): Be more detailed: version, link to Maven Central
+
+from __future__ import print_function
+
+from collections import defaultdict, deque
+import re
+from shutil import copyfileobj
+from subprocess import Popen, PIPE
+from sys import stdout
+
+MAIN = ['//gerrit-pgm:pgm', '//gerrit-gwtui:ui_module']
+
+def parse_graph():
+ graph = defaultdict(list)
+ p = Popen(
+ ['buck', 'audit', 'classpath', '--dot'] + MAIN,
+ stdout = PIPE)
+ for line in p.stdout:
+ m = re.search(r'"(//.*?)" -> "(//.*?)";', line)
+ if not m:
+ continue
+ target, dep = m.group(1), m.group(2)
+ if not target.endswith('__compile'):
+ graph[target].append(dep)
+ r = p.wait()
+ if r != 0:
+ exit(r)
+ return graph
+
+graph = parse_graph()
+licenses = defaultdict(set)
+
+queue = deque(MAIN)
+while queue:
+ target = queue.popleft()
+ for dep in graph[target]:
+ if not dep.startswith('//lib:LICENSE-'):
+ continue
+ licenses[dep].add(target)
+ queue.extend(graph[target])
+used = sorted(licenses.keys())
+
+print("""\
+Gerrit Code Review - Licenses
+=============================
+
+Gerrit open source software is licensed under the <<Apache2_0,Apache
+License 2.0>>. Executable distributions also include other software
+components that are provided under additional licenses.
+
+[[cryptography]]
+Cryptography Notice
+-------------------
+
+This distribution includes cryptographic software. The country
+in which you currently reside may have restrictions on the import,
+possession, use, and/or re-export to another country, of encryption
+software. BEFORE using any encryption software, please check
+your country's laws, regulations and policies concerning the
+import, possession, or use, and re-export of encryption software,
+to see if this is permitted. See the
+link:http://www.wassenaar.org/[Wassenaar Arrangement]
+for more information.
+
+The U.S. Government Department of Commerce, Bureau of Industry
+and Security (BIS), has classified this software as Export
+Commodity Control Number (ECCN) 5D002.C.1, which includes
+information security software using or performing cryptographic
+functions with asymmetric algorithms. The form and manner of
+this distribution makes it eligible for export under the License
+Exception ENC Technology Software Unrestricted (TSU) exception
+(see the BIS Export Administration Regulations, Section 740.13)
+for both object code and source code.
+
+Gerrit includes an SSH daemon (Apache SSHD), to support authenticated
+uploads of changes directly from `git push` command line clients.
+
+Gerrit includes an SSH client (JSch), to support authenticated
+replication of changes to remote systems, such as for automatic
+updates of mirror servers, or realtime backups.
+
+For either feature to function, Gerrit requires the
+link:http://java.sun.com/javase/technologies/security/[Java Cryptography extensions]
+and/or the
+link:http://www.bouncycastle.org/java.html[Bouncy Castle Crypto API]
+to be installed by the end-user.
+
+Licenses
+--------
+""")
+
+for n in used:
+ libs = sorted(licenses[n])
+ name = n[len('//lib:LICENSE-'):]
+ print()
+ print('[[%s]]' % name.replace('.', '_'))
+ print(name)
+ print('~' * len(name))
+ print()
+ for d in libs:
+ if d.startswith('//lib:') or d.startswith('//lib/'):
+ p = d[len('//lib:'):]
+ else:
+ p = d[d.index(':')+1:].lower()
+ print('* ' + p)
+ print()
+ print('----')
+ with open(n[2:].replace(':', '/')) as fd:
+ copyfileobj(fd, stdout)
+ print('----')
+
+print("""
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+""")
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 88b50fa..28e0184 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -1,72 +1,84 @@
Gerrit Code Review for Git
==========================
-Getting Started
----------------
+Index
+-----
-* link:intro-quick.html[A Quick Introduction To Gerrit]
+. General info
+.. link:licenses.html[Licenses and Notices]
+. Installing
+.. link:intro-quick.html[A Quick Introduction To Gerrit]
+.. link:install.html[Installation Guide]
+. Tutorial
+.. Get started
+... External link: link:http://source.android.com/submit-patches/workflow[Default Android Workflow]
+.. Web
+... Registering a new Gerrit account
+... link:user-search.html[Searching Changes]
+... link:user-notify.html[Subscribing to Email Notifications]
+.. Ssh
+... ssh connection details
+... link:cmd-index.html[Command Line Tools]
+.. Git
+... git connection details
+... Commands, scenarios
+.... link:user-upload.html[Uploading Changes]
+.... link:error-messages.html[Error Messages]
+... Changes
+.... link:user-changeid.html[Change-Id Lines]
+.... link:user-signedoffby.html[Signed-off-by Lines]
+... Patch sets
+. Project management
+.. link:project-setup.html[Project Setup]
+.. link:access-control.html[Access Controls]
+... link:config-labels.html[Review Labels]
+.. Multi-project management
+... Submodules
+... Repo
+.. Prolog rules
+... link:prolog-cookbook.html[Prolog Cookbook]
+... link:prolog-change-facts.html[Prolog Facts for Gerrit Changes]
+.. link:user-submodules.html[Subscribing to Git Submodules]
+.. Project sunset
+. Customization and integration
+.. link:user-dashboards.html[Dashboards]
+.. link:rest-api.html[REST API]
+.. link:config-gitweb.html[Gitweb Integration]
+.. link:config-themes.html[Themes]
+.. link:config-sso.html[Single Sign-On Systems]
+.. link:config-hooks.html[Hooks]
+.. link:config-mail.html[Mail Templates]
+.. link:config-cla.html[Contributor Agreements]
+. Server administration
+.. link:config-gerrit.html[System Settings]
+.. Backup
+.. Performance tuning
+... link:cmd-index.html[Command Line Tools]
+... Reading show-caches efficiently
+... How to read stats from the JVM
+.. High availability
+.. Replication
+.. Plugins
+.. link:dev-design.html[System Design]
+.. link:config-contact.html[User Contact Information]
+.. link:config-reverseproxy.html[Reverse Proxy]
+.. link:pgm-index.html[Server Side Administrative Tools]
+. Developer
+.. link:dev-readme.html[Developer Setup]
+.. link:dev-buck.html[Building with Buck]
+.. link:dev-eclipse.html[Eclipse Setup]
+.. link:dev-contributing.html[Contributing to Gerrit]
+.. Documentation formatting guide for contributions
+.. link:dev-design.html[System Design]
+.. link:i18n-readme.html[i18n Support]
+.. Plugin development
+... link:dev-plugins.html[Developing Plugins]
+... link:js-api.html[JavaScript Plugin API]
+... link:config-validation.html[Commit Validation]
+. Maintainer
+.. link:dev-release.html[Developer Release]
+.. link:dev-release-subproject.html[Developer Subproject Release]
-End User Guide
---------------
-
-* External link: link:http://source.android.com/submit-patches/workflow[Default Android Workflow]
-* link:user-search.html[Searching Changes]
-* link:cmd-index.html[Command Line Tools]
-* link:user-upload.html[Uploading Changes]
-* link:user-changeid.html[Change-Id Lines]
-* link:user-signedoffby.html[Signed-off-by Lines]
-* link:error-messages.html[Error Messages]
-* link:user-notify.html[Subscribing to Email Notifications]
-
-Project Owner and Power User Guide
-----------------------------------
-
-* link:access-control.html[Access Controls]
-* link:rest-api.html[REST API]
-* link:user-dashboards.html[Dashboards]
-* link:user-submodules.html[Subscribing to Git Submodules]
-* link:prolog-cookbook.html[Prolog Cookbook]
-* link:prolog-change-facts.html[Prolog Facts for Gerrit Changes]
-* link:config-labels.html[Review Labels]
-
-Admin User Guide
-----------------
-
-* link:pgm-index.html[Server Side Administrative Tools]
-
-Installation
-~~~~~~~~~~~~
-
-* link:licenses.html[Licenses and Notices]
-* link:install.html[Installation Guide]
-* link:install-quick.html[Quick Installation in 10 Minutes]
-* link:project-setup.html[Project Setup]
-
-Configuration
-~~~~~~~~~~~~~
-
-* link:config-gerrit.html[System Settings]
-* link:config-contact.html[User Contact Information]
-* link:config-gitweb.html[Gitweb Integration]
-* link:config-themes.html[Themes]
-* link:config-sso.html[Single Sign-On Systems]
-* link:config-reverseproxy.html[Reverse Proxy]
-* link:config-hooks.html[Hooks]
-* link:config-mail.html[Mail Templates]
-* link:config-cla.html[Contributor Agreements]
-
-Gerrit Developer Documentation
-------------------------------
-
-* link:dev-readme.html[Developer Setup]
-* link:dev-eclipse.html[Eclipse Setup]
-* link:dev-contributing.html[Contributing to Gerrit]
-* link:dev-plugins.html[Developing Plugins]
-* link:config-validation.html[Commit Validation]
-* link:dev-design.html[System Design]
-* link:i18n-readme.html[i18n Support]
-* link:dev-release.html[Developer Release]
-* link:dev-release-subproject.html[Developer Subproject Release]
Resources
---------
diff --git a/Documentation/install-j2ee.txt b/Documentation/install-j2ee.txt
index 4927041..f6853c8 100644
--- a/Documentation/install-j2ee.txt
+++ b/Documentation/install-j2ee.txt
@@ -21,7 +21,7 @@
link:install.html#init[site initialization] tasks described
in the standard installation documentation.
-* Stop the embedded deamon that was automatically started by 'init':
+* Stop the embedded daemon that was automatically started by 'init':
+
----
review_site/bin/gerrit.sh stop
diff --git a/Documentation/install.txt b/Documentation/install.txt
index a18a506..8137f53 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -9,7 +9,7 @@
* JDK, minimum version 1.6 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download]
-You'll also need a SQL database to house the review metadata. You have the
+You'll also need an SQL database to house the review metadata. You have the
choice of either using the embedded H2 or to host your own MySQL or PostgreSQL.
diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt
new file mode 100644
index 0000000..9e0c308
--- /dev/null
+++ b/Documentation/js-api.txt
@@ -0,0 +1,633 @@
+Gerrit Code Review - JavaScript API
+===================================
+
+Gerrit Code Review supports an API for JavaScript plugins to interact
+with the web UI and the server process.
+
+Entry Point
+-----------
+
+JavaScript is loaded using a standard `<script src='...'>` HTML tag.
+Plugins should protect the global namespace by defining their code
+within an anonymous function passed to `Gerrit.install()`. The plugin
+will be passed an object describing its registration with Gerrit:
+
+[source,javascript]
+----
+Gerrit.install(function (self) {
+ // ... plugin JavaScript code here ...
+});
+----
+
+
+[[self]]
+Plugin Instance
+---------------
+
+The plugin instance is passed to the plugin's initialization function
+and provides a number of utility services to plugin authors.
+
+[[self_delete]]
+self.delete()
+~~~~~~~~~~~~~
+Issues a DELETE REST API request to the Gerrit server.
+
+.Signature
+[source,javascript]
+----
+Gerrit.delete(url, callback)
+----
+
+* url: URL relative to the plugin's URL space. The JavaScript
+ library prefixes the supplied URL with `/plugins/{getPluginName}/`.
+
+* callback: JavaScript function to be invoked with the parsed
+ JSON result of the API call. DELETE methods often return
+ `204 No Content`, which is passed as null.
+
+[[self_get]]
+self.get()
+~~~~~~~~~~
+Issues a GET REST API request to the Gerrit server.
+
+.Signature
+[source,javascript]
+----
+self.get(url, callback)
+----
+
+* url: URL relative to the plugin's URL space. The JavaScript
+ library prefixes the supplied URL with `/plugins/{getPluginName}/`.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+ result of the API call. If the API returns a string the result is
+ a string, otherwise the result is a JavaScript object or array,
+ as described in the relevant REST API documentation.
+
+[[self_getPluginName]]
+self.getPluginName()
+~~~~~~~~~~~~~~~~~~~~
+Returns the name this plugin was installed as by the server
+administrator. The plugin name is required to access REST API
+views installed by the plugin, or to access resources.
+
+[[self_post]]
+self.post()
+~~~~~~~~~~~
+Issues a POST REST API request to the Gerrit server.
+
+.Signature
+[source,javascript]
+----
+self.post(url, input, callback)
+----
+
+* url: URL relative to the plugin's URL space. The JavaScript
+ library prefixes the supplied URL with `/plugins/{getPluginName}/`.
+
+* input: JavaScript object to serialize as the request payload.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+ result of the API call. If the API returns a string the result is
+ a string, otherwise the result is a JavaScript object or array,
+ as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+self.post(
+ '/my-servlet',
+ {start_build: true, platform_type: 'Linux'},
+ function (r) {});
+----
+
+[[self_put]]
+self.put()
+~~~~~~~~~~
+Issues a PUT REST API request to the Gerrit server.
+
+.Signature
+[source,javascript]
+----
+self.put(url, input, callback)
+----
+
+* url: URL relative to the plugin's URL space. The JavaScript
+ library prefixes the supplied URL with `/plugins/{getPluginName}/`.
+
+* input: JavaScript object to serialize as the request payload.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+ result of the API call. If the API returns a string the result is
+ a string, otherwise the result is a JavaScript object or array,
+ as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+self.put(
+ '/builds',
+ {start_build: true, platform_type: 'Linux'},
+ function (r) {});
+----
+
+[[self_onAction]]
+self.onAction()
+~~~~~~~~~~~~~~~
+Register a JavaScript callback to be invoked when the user clicks
+on a button associated with a server side `UiAction`.
+
+.Signature
+[source,javascript]
+----
+Gerrit.onAction(type, view_name, callback);
+----
+
+* type: `'change'` or `'revision'`, indicating what sort of resource
+ the `UiAction` was bound to in the server.
+
+* view_name: string appearing in URLs to name the view. This is the
+ second argument of the `get()`, `post()`, `put()`, and `delete()`
+ binding methods in a `RestApiModule`.
+
+* callback: JavaScript function to invoke when the user clicks. The
+ function will be passed a link:#ActionContext[action context].
+
+[[self_url]]
+self.url()
+~~~~~~~~~~
+Returns a URL within the plugin's URL space. If invoked with no
+parameter the URL of the plugin is returned. If passed a string
+the argument is appended to the plugin URL.
+
+[source,javascript]
+----
+self.url(); // "https://gerrit-review.googlesource.com/plugins/demo/"
+self.url('/static/icon.png'); // "https://gerrit-review.googlesource.com/plugins/demo/static/icon.png"
+----
+
+
+[[ActionContext]]
+Action Context
+--------------
+A new action context is passed to the `onAction` callback function
+each time the associated action button is clicked by the user. A
+context is initialized with sufficient state to issue the associated
+REST API RPC.
+
+[[context_action]]
+context.action
+~~~~~~~~~~~~~~
+An link:rest-api-changes.html#action-info[ActionInfo] object instance
+supplied by the server describing the UI button the user used to
+invoke the action.
+
+[[context_call]]
+context.call()
+~~~~~~~~~~~~~~
+Issues the REST API call associated with the action. The HTTP method
+used comes from `context.action.method`, hiding the JavaScript from
+needing to care.
+
+.Signature
+[source,javascript]
+----
+context.call(input, callback)
+----
+
+* input: JavaScript object to serialize as the request payload. This
+ parameter is ignored for GET and DELETE methods.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+ result of the API call. If the API returns a string the result is
+ a string, otherwise the result is a JavaScript object or array,
+ as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+context.call(
+ {message: "..."},
+ function (result) {
+ // ... use result here ...
+ });
+----
+
+[[context_change]]
+context.change
+~~~~~~~~~~~~~~
+When the action is invoked on a change a
+link:rest-api-changes.html#change-info[ChangeInfo] object instance
+describing the change. Available fields of the ChangeInfo may vary
+based on the options used by the UI when it loaded the change.
+
+[[context_delete]]
+context.delete()
+~~~~~~~~~~~~~~~~
+Issues a DELETE REST API call to the URL associated with the action.
+
+.Signature
+[source,javascript]
+----
+context.delete(callback)
+----
+
+* callback: JavaScript function to be invoked with the parsed
+ JSON result of the API call. DELETE methods often return
+ `204 No Content`, which is passed as null.
+
+[source,javascript]
+----
+context.delete(function () {});
+----
+
+[[context_get]]
+context.get()
+~~~~~~~~~~~~~
+Issues a GET REST API call to the URL associated with the action.
+
+.Signature
+[source,javascript]
+----
+context.get(callback)
+----
+
+* callback: JavaScript function to be invoked with the parsed JSON
+ result of the API call. If the API returns a string the result is
+ a string, otherwise the result is a JavaScript object or array,
+ as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+context.get(function (result) {
+ // ... use result here ...
+});
+----
+
+[[context_go]]
+context.go()
+~~~~~~~~~~~~
+Go to a page. Shorthand for link:#Gerrit_go[`Gerrit.go()`].
+
+[[context_hide]]
+context.hide()
+~~~~~~~~~~~~~~
+Hide the currently visible popup displayed by
+link:#context_popup[`context.popup()`].
+
+[[context_post]]
+context.post()
+~~~~~~~~~~~~~~
+Issues a POST REST API call to the URL associated with the action.
+
+.Signature
+[source,javascript]
+----
+context.post(input, callback)
+----
+
+* input: JavaScript object to serialize as the request payload.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+ result of the API call. If the API returns a string the result is
+ a string, otherwise the result is a JavaScript object or array,
+ as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+context.post(
+ {message: "..."},
+ function (result) {
+ // ... use result here ...
+ });
+----
+
+[[context_popup]]
+context.popup()
+~~~~~~~~~~~~~~~
+
+Displays a small popup near the activation button to gather
+additional input from the user before executing the REST API RPC.
+
+The caller is always responsible for closing the popup with
+link#context_hide[`context.hide()`]. Gerrit will handle closing a
+popup if the user presses `Escape` while keyboard focus is within
+the popup.
+
+.Signature
+[source,javascript]
+----
+context.popup(element)
+----
+
+* element: an HTML DOM element to display as the body of the
+ popup. This is typically a `div` element but can be any valid HTML
+ element. CSS can be used to style the element beyond the defaults.
+
+A common usage is to gather more input:
+
+[source,javascript]
+----
+self.onAction('revision', 'start-build', function (c) {
+ var l = c.checkbox();
+ var m = c.checkbox();
+ c.popup(c.div(
+ c.div(c.label(l, 'Linux')),
+ c.div(c.label(m, 'Mac OS X')),
+ c.button('Build', {onclick: function() {
+ c.call(
+ {
+ commit: c.revision.name,
+ linux: l.checked,
+ mac: m.checked,
+ },
+ function() { c.hide() });
+ });
+});
+----
+
+[[context_put]]
+context.put()
+~~~~~~~~~~~~~
+Issues a PUT REST API call to the URL associated with the action.
+
+.Signature
+[source,javascript]
+----
+context.put(input, callback)
+----
+
+* input: JavaScript object to serialize as the request payload.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+ result of the API call. If the API returns a string the result is
+ a string, otherwise the result is a JavaScript object or array,
+ as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+context.put(
+ {message: "..."},
+ function (result) {
+ // ... use result here ...
+ });
+----
+
+[[context_refresh]]
+context.refresh()
+~~~~~~~~~~~~~~~~~
+Refresh the current display. Shorthand for
+link:#Gerrit_refresh[`Gerrit.refresh()`].
+
+[[context_revision]]
+context.revision
+~~~~~~~~~~~~~~~~
+When the action is invoked on a specific revision of a change,
+a link:rest-api-changes.html#revision-info[RevisionInfo]
+object instance describing the revision. Available fields of the
+RevisionInfo may vary based on the options used by the UI when it
+loaded the change.
+
+
+Action Context HTML Helpers
+---------------------------
+The link:#ActionContext[action context] includes some HTML helper
+functions to make working with DOM based widgets less painful.
+
+* `br()`: new `<br>` element.
+
+* `button(label, options)`: new `<button>` with the string `label`
+ wrapped inside of a `div`. The optional `options` object may
+ define `onclick` as a function to be invoked upon clicking. This
+ calling pattern avoids circular references between the element
+ and the onclick handler.
+
+* `checkbox()`: new `<input type='checkbox'>` element.
+* `div(...)`: a new `<div>` wrapping the (optional) arguments.
+* `hr()`: new `<hr>` element.
+
+* `label(c, label)`: a new `<label>` element wrapping element `c`
+ and the string `label`. Used to wrap a checkbox with its label,
+ `label(checkbox(), 'Click Me')`.
+
+* `prependLabel(label, c)`: a new `<label>` element wrapping element `c`
+ and the string `label`. Used to wrap an input field with its label,
+ `prependLabel('Greeting message', textfield())`.
+
+* `textarea(options)`: new `<textarea>` element. The options
+ object may optionally include `rows` and `cols`. The textarea
+ comes with an onkeypress handler installed to play nicely with
+ Gerrit's keyboard binding system.
+
+* `textfield()`: new `<input type='text'>` element. The text field
+ comes with an onkeypress handler installed to play nicely with
+ Gerrit's keyboard binding system.
+
+* `span(...)`: a new `<span>` wrapping the (optional) arguments.
+
+* `msg(label)`: a new label.
+
+[[Gerrit]]
+Gerrit
+------
+
+The `Gerrit` object is the only symbol provided into the global
+namespace by Gerrit Code Review. All top-level functions can be
+accessed through this name.
+
+[[Gerrit_delete]]
+Gerrit.delete()
+~~~~~~~~~~~~~~~
+Issues a DELETE REST API request to the Gerrit server. For plugin
+private REST API URLs see link:#self_delete[self.delete()].
+
+.Signature
+[source,javascript]
+----
+Gerrit.delete(url, callback)
+----
+
+* url: URL relative to the Gerrit server. For example to access the
+ link:rest-api-changes.html[changes REST API] use `'/changes/'`.
+
+* callback: JavaScript function to be invoked with the parsed
+ JSON result of the API call. DELETE methods often return
+ `204 No Content`, which is passed as null.
+
+[source,javascript]
+----
+Gerrit.delete(
+ '/changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/topic',
+ function () {});
+----
+
+[[Gerrit_get]]
+Gerrit.get()
+~~~~~~~~~~~~
+Issues a GET REST API request to the Gerrit server. For plugin
+private REST API URLs see link:#self_get[self.get()].
+
+.Signature
+[source,javascript]
+----
+Gerrit.get(url, callback)
+----
+
+* url: URL relative to the Gerrit server. For example to access the
+ link:rest-api-changes.html[changes REST API] use `'/changes/'`.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+ result of the API call. If the API returns a string the result is
+ a string, otherwise the result is a JavaScript object or array,
+ as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+Gerrit.get('/changes/?q=status:open', function (open) {
+ for (var i = 0; i < open.length; i++) {
+ console.log(open.get(i).change_id);
+ }
+});
+----
+
+[[Gerrit_getPluginName]]
+Gerrit.getPluginName()
+~~~~~~~~~~~~~~~~~~~~~~
+Returns the name this plugin was installed as by the server
+administrator. The plugin name is required to access REST API
+views installed by the plugin, or to access resources.
+
+Unlike link:#self_getPluginName[`self.getPluginName()`] this method
+must guess the name from the JavaScript call stack. Plugins are
+encouraged to use `self.getPluginName()` whenever possible.
+
+[[Gerrit_go]]
+Gerrit.go()
+~~~~~~~~~~~
+Updates the web UI to display the view identified by the supplied
+URL token. The URL token is the text after `#` in the browser URL.
+
+[source,javascript]
+----
+Gerrit.go('/admin/projects/');
+----
+
+If the URL passed matches `http://...`, `https://...`, or `//...`
+the current browser window will navigate to the non-Gerrit URL.
+The user can return to Gerrit with the back button.
+
+[[Gerrit_install]]
+Gerrit.install()
+~~~~~~~~~~~~~~~~
+Registers a new plugin by invoking the supplied initialization
+function. The function is passed the link:#self[plugin instance].
+
+[source,javascript]
+----
+Gerrit.install(function (self) {
+ // ... plugin JavaScript code here ...
+});
+----
+
+[[Gerrit_post]]
+Gerrit.post()
+~~~~~~~~~~~~~
+Issues a POST REST API request to the Gerrit server. For plugin
+private REST API URLs see link:#self_post[self.post()].
+
+.Signature
+[source,javascript]
+----
+Gerrit.post(url, input, callback)
+----
+
+* url: URL relative to the Gerrit server. For example to access the
+ link:rest-api-changes.html[changes REST API] use `'/changes/'`.
+
+* input: JavaScript object to serialize as the request payload.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+ result of the API call. If the API returns a string the result is
+ a string, otherwise the result is a JavaScript object or array,
+ as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+Gerrit.post(
+ '/changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/topic',
+ {topic: 'tests', message: 'Classify work as for testing.'},
+ function (r) {});
+----
+
+[[Gerrit_put]]
+Gerrit.put()
+~~~~~~~~~~~~
+Issues a PUT REST API request to the Gerrit server. For plugin
+private REST API URLs see link:#self_put[self.put()].
+
+.Signature
+[source,javascript]
+----
+Gerrit.put(url, input, callback)
+----
+
+* url: URL relative to the Gerrit server. For example to access the
+ link:rest-api-changes.html[changes REST API] use `'/changes/'`.
+
+* input: JavaScript object to serialize as the request payload.
+
+* callback: JavaScript function to be invoked with the parsed JSON
+ result of the API call. If the API returns a string the result is
+ a string, otherwise the result is a JavaScript object or array,
+ as described in the relevant REST API documentation.
+
+[source,javascript]
+----
+Gerrit.put(
+ '/changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/topic',
+ {topic: 'tests', message: 'Classify work as for testing.'},
+ function (r) {});
+----
+
+[[Gerrit_onAction]]
+Gerrit.onAction()
+~~~~~~~~~~~~~~~~~
+Register a JavaScript callback to be invoked when the user clicks
+on a button associated with a server side `UiAction`.
+
+.Signature
+[source,javascript]
+----
+Gerrit.onAction(type, view_name, callback);
+----
+
+* type: `'change'` or `'revision'`, indicating what sort of resource
+ the `UiAction` was bound to in the server.
+
+* view_name: string appearing in URLs to name the view. This is the
+ second argument of the `get()`, `post()`, `put()`, and `delete()`
+ binding methods in a `RestApiModule`.
+
+* callback: JavaScript function to invoke when the user clicks. The
+ function will be passed a link:#ActionContext[ActionContext].
+
+[[Gerrit_refresh]]
+Gerrit.refresh()
+~~~~~~~~~~~~~~~~
+Redisplays the current web UI view, refreshing all information.
+
+[[Gerrit_url]]
+Gerrit.url()
+~~~~~~~~~~~~
+Returns the URL of the Gerrit Code Review server. If invoked with
+no parameter the URL of the site is returned. If passed a string
+the argument is appended to the site URL.
+
+[source,javascript]
+----
+Gerrit.url(); // "https://gerrit-review.googlesource.com/"
+Gerrit.url('/123'); // "https://gerrit-review.googlesource.com/123"
+----
+
+For a plugin specific version see link:#self_url()[`self.url()`].
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/json.txt b/Documentation/json.txt
index 3893e3f..2836e5b 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -115,11 +115,13 @@
createdOn:: Time in seconds since the UNIX epoch when this patchset
was created.
+isDraft:: Whether or not the patch set is a draft patch set.
+
approvals:: The <<approval,approval attribute>> granted.
comments:: All comments for this patchset in <<patchsetcomment,patchsetComment attributes>>.
-files:: All changed files in this patchset in <<patch,patch attributes>>.
+files:: All changed files in this patchset in <<file,file attributes>>.
sizeInsertions:: Size information of insertions of this patchset.
@@ -235,9 +237,9 @@
message:: The comment text.
-[[patch]]
-patch
------
+[[file]]
+file
+----
Information about a patch on a file.
file:: The name of the file. If the file is renamed, the new name.
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
deleted file mode 100644
index fbdd9cd..0000000
--- a/Documentation/licenses.txt
+++ /dev/null
@@ -1,1189 +0,0 @@
-Gerrit Code Review - Licenses
-=============================
-
-Gerrit itself is licensed under the <<apache2,Apache License 2.0>>.
-Gerrit's executable distributions also include many other software
-components that are covered by additional licenses.
-
-Included Components
--------------------
-
-[options="header"]
-|======================================================================
-|Included Package | License
-|Gerrit Code Review | <<apache2,Apache License 2.0>>
-|gwtjsonrpc | <<apache2,Apache License 2.0>>
-|gwtorm | <<apache2,Apache License 2.0>>
-|Google Gson | <<apache2,Apache License 2.0>>
-|Google Web Toolkit | <<apache2,Apache License 2.0>>
-|Guice | <<apache2,Apache License 2.0>>
-|Guava Libraries | <<apache2,Apache License 2.0>>
-|Apache Commons Codec | <<apache2,Apache License 2.0>>
-|Apache Commons DBCP | <<apache2,Apache License 2.0>>
-|Apache Commons Http Client | <<apache2,Apache License 2.0>>
-|Apache Commons Lang | <<apache2,Apache License 2.0>>
-|Apache Commons Logging | <<apache2,Apache License 2.0>>
-|Apache Commons Net | <<apache2,Apache License 2.0>>
-|Apache Commons Pool | <<apache2,Apache License 2.0>>
-|Apache Log4J | <<apache2,Apache License 2.0>>
-|Apache MINA | <<apache2,Apache License 2.0>>
-|Apache Tomcat Servlet API | <<apache2,Apache License 2.0>>
-|Apache SSHD | <<apache2,Apache License 2.0>>, see also <<sshd,NOTICE>>
-|Apache Velocity | <<apache2,Apache License 2.0>>
-|Apache Xerces | <<apache2,Apache License 2.0>>
-|OpenID4Java | <<apache2,Apache License 2.0>>
-|Neko HTML | <<apache2,Apache License 2.0>>
-|mime-util | <<apache2,Apache License 2.0>>
-|Jetty | <<apache2,Apache License 2.0>>, or link:http://www.eclipse.org/legal/epl-v10.html[EPL]
-|Prolog Cafe | <<prolog_cafe,EPL or GPL>>
-|Google Code Prettify | <<apache2,Apache License 2.0>>
-|JavaEWAH | <<apache2,Apache License 2.0>>
-|JGit | <<jgit,New-Style BSD>>
-|JSch | <<sshd,New-Style BSD>>
-|PostgreSQL JDBC Driver | <<postgresql,New-Style BSD>>
-|H2 Database | <<h2,EPL or modified MPL>>
-|ObjectWeb ASM | <<asm,New-Style BSD>>
-|ANTLR | <<antlr,New-Style BSD>>
-|args4j | <<args4j,MIT License>>
-|SLF4J | <<slf4j,MIT License>>
-|Clippy | <<clippy,MIT License>>
-|juniversalchardet | <<mpl1_1,MPL 1.1>>
-|AOP Alliance | Public Domain
-|JSR 305 | <<jsr305,New-Style BSD>>
-|dk.brics.automaton | <<automaton,New-Style BSD>>
-|Java Concurrency in Practice Annotations | <<jcip,Create Commons Attribution License>>
-|pegdown | <<apache2,Apache License 2.0>>
-|======================================================================
-
-Cryptography Notice
--------------------
-
-This distribution includes cryptographic software. The country
-in which you currently reside may have restrictions on the import,
-possession, use, and/or re-export to another country, of encryption
-software. BEFORE using any encryption software, please check
-your country's laws, regulations and policies concerning the
-import, possession, or use, and re-export of encryption software,
-to see if this is permitted. See the
-link:http://www.wassenaar.org/[Wassenaar Arrangement]
-for more information.
-
-The U.S. Government Department of Commerce, Bureau of Industry
-and Security (BIS), has classified this software as Export
-Commodity Control Number (ECCN) 5D002.C.1, which includes
-information security software using or performing cryptographic
-functions with asymmetric algorithms. The form and manner of
-this distribution makes it eligible for export under the License
-Exception ENC Technology Software Unrestricted (TSU) exception
-(see the BIS Export Administration Regulations, Section 740.13)
-for both object code and source code.
-
-Gerrit includes an SSH daemon (Apache SSHD), to support authenticated
-uploads of changes directly from `git push` command line clients.
-
-Gerrit includes an SSH client (JSch), to support authenticated
-replication of changes to remote systems, such as for automatic
-updates of mirror servers, or realtime backups.
-
-For either feature to function, Gerrit requires the
-link:http://java.sun.com/javase/technologies/security/[Java Cryptography extensions]
-and/or the
-link:http://www.bouncycastle.org/java.html[Bouncy Castle Crypto API]
-to be installed by the end-user.
-
-Licenses
---------
-
-[[apache2]]
-Apache License 2.0
-~~~~~~~~~~~~~~~~~~
-
-----
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-----
-
-[[sshd]]
-Apache SSHD - Notice
-~~~~~~~~~~~~~~~~~~~~
-
-* link:http://svn.apache.org/viewvc/mina/sshd/trunk/NOTICE.txt?view=markup[Original]
-
-----
- =========================================================================
- == NOTICE file for use with the Apache License, Version 2.0, ==
- == in this case for the SSHD distribution. ==
- =========================================================================
-
- This product contains software developped by JCraft,Inc. and subject to
- the following license:
-
-Copyright (c) 2002,2003,2004,2005,2006,2007,2008 Atsuhiko Yamanaka, JCraft,Inc.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
- 1. Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
-
- 2. Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in
- the documentation and/or other materials provided with the distribution.
-
- 3. The names of the authors may not be used to endorse or promote products
- derived from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
-INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
-INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
-INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
-OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
-LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
-EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
- --------------------------------------------------------------------------------
-
-Copyright (c) 2000 - 2006 The Legion Of The Bouncy Castle (http://www.bouncycastle.org)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in the
-Software without restriction, including without limitation the rights to use, copy,
-modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
-and to permit persons to whom the Software is furnished to do so, subject to the
-following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
-PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-----
-
-[[postgresql]]
-PostgreSQL JDBC Driver - New Style BSD
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://jdbc.postgresql.org/license.html[Original]
-
-----
-Copyright (c) 1997-2008, PostgreSQL Global Development Group
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-1. Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
-2. Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
-3. Neither the name of the PostgreSQL Global Development Group nor the names
- of its contributors may be used to endorse or promote products derived
- from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
-----
-
-[[prolog_cafe]]
-Prolog Cafe - EPL or GPL
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-Originally developed by Mutsunori BANBARA and Naoyuki TAMURA at the
-Kobe University, JAPAN. Gerrit Code Review uses a fork derived from
-the 1.2.5 release, and offers the corresponding source code at
-link:https://gerrit.googlesource.com/prolog-cafe[].
-
-Prolog Cafe is dual licensed and available under either the
-link:http://opensource.org/licenses/eclipse-1.0.php[Eclipse Public License],
-or the
-link:http://www.gnu.org/licenses/gpl-2.0.html[GPL version 2.0 (or later)].
-
-In the context of Gerrit Code Review, Prolog Cafe is consumed
-under the EPL.
-
-[[h2]]
-H2 Database - EPL or modified MPL
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://www.h2database.com/html/license.html[Complete Terms]
-
-H2 is dual licensed and available under a modified version of the
-MPL 1.1 (Mozilla Public License) or under the (unmodified) EPL 1.0
-(link:http://opensource.org/licenses/eclipse-1.0.php[Eclipse Public License]).
-
-[[asm]]
-ObjectWeb ASM - New Style BSD
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://svn.forge.objectweb.org/cgi-bin/viewcvs.cgi/asm/trunk/asm/LICENSE.txt[Original]
-
-----
- ASM: a very small and fast Java bytecode manipulation framework
- Copyright (c) 2000-2005 INRIA, France Telecom
- All rights reserved.
-
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions
- are met:
-
- 1. Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
- 2. Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
- 3. Neither the name of the copyright holders nor the names of its
- contributors may be used to endorse or promote products derived from
- this software without specific prior written permission.
-
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
- LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
- THE POSSIBILITY OF SUCH DAMAGE.
-----
-
-[[antlr]]
-ANTLR - New Style BSD
-~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://www.antlr.org/license.html[Original]
-
-----
-Copyright (c) 2003-2008, Terence Parr
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions
-are met:
-
- * Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above
- copyright notice, this list of conditions and the following
- disclaimer in the documentation and/or other materials provided
- with the distribution.
- * Neither the name of the author nor the names of its
- contributors may be used to endorse or promote products derived
- from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
-COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
-ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
-----
-
-[[jgit]]
-JGit - New Style BSD
-~~~~~~~~~~~~~~~~~~~~
-
-* link:http://repo.or.cz/w/egit.git?a=blob;f=org.spearce.jgit/LICENSE;hb=HEAD[Original]
-
-----
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or
- * without modification, are permitted provided that the following
- * conditions are met:
- *
- * - Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following
- * disclaimer in the documentation and/or other materials provided
- * with the distribution.
- *
- * - Neither the name of the Git Development Community nor the
- * names of its contributors may be used to endorse or promote
- * products derived from this software without specific prior
- * written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-----
-
-[[jsr305]]
-JSR 305 Reference Implementation - New Style BSD
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://code.google.com/p/jsr-305/source/browse/trunk/ri/LICENSE[Original 1]
-* link:http://code.google.com/p/findbugs/source/browse/trunk/findbugs/LICENSE-jsr305.txt[Original 2]
-
-----
-Copyright (c) 2007-2009, JSR305 expert group
-All rights reserved.
-
-http://www.opensource.org/licenses/bsd-license.php
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
- * Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
- * Neither the name of the JSR305 expert group nor the names of its
- contributors may be used to endorse or promote products derived from
- this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
-----
-
-[[automaton]]
-dk.brics.automaton - New Style BSD
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://www.brics.dk/automaton/index.html
-
-----
-Copyright (c) 2007-2009, dk.brics.automaton
-All rights reserved.
-
-http://www.opensource.org/licenses/bsd-license.php
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
- * Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
- * Neither the name of the JSR305 expert group nor the names of its
- contributors may be used to endorse or promote products derived from
- this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
-----
-
-[[args4j]]
-args4j - MIT License
-~~~~~~~~~~~~~~~~~~~~
-
-* link:https://args4j.dev.java.net/[Home]
-* link:http://www.opensource.org/licenses/mit-license.php[Canonical MIT License]
-
-[[slf4j]]
-SLF4J - MIT License
-~~~~~~~~~~~~~~~~~~~
-
-* link:http://www.slf4j.org/license.html[Original]
-
-----
- Copyright (c) 2004-2008 QOS.ch
- All rights reserved.
-
- Permission is hereby granted, free of charge, to any person obtaining
- a copy of this software and associated documentation files (the
- "Software"), to deal in the Software without restriction, including
- without limitation the rights to use, copy, modify, merge, publish,
- distribute, sublicense, and/or sell copies of the Software, and to
- permit persons to whom the Software is furnished to do so, subject to
- the following conditions:
-
- The above copyright notice and this permission notice shall be
- included in all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-----
-
-[[clippy]]
-Clippy - MIT License
-~~~~~~~~~~~~~~~~~~~~
-
-* link:http://github.com/mojombo/clippy/tree/master[Site]
-
-----
-(The MIT License)
-
-Copyright (c) 2008 Tom Preston-Werner
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-'Software'), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-----
-
-[[jcip]]
-Java Concurrency in Practice Annotations
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://jcip.net/[book website]
-* link:http://jcip.net/jcip-annotations-src.jar[sources]
-* link:http://creativecommons.org/licenses/by/2.5/[license]
-
-----
-Copyright (c) 2005 Brian Goetz and Tim Peierls
-Released under the Creative Commons Attribution License
- (http://creativecommons.org/licenses/by/2.5)
-Official home: http://www.jcip.net
-
-Any republication or derived work distributed in source code form
-must include this copyright and license notice.
-----
-
-[[mpl1_1]]
-Mozilla Public License 1.1
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* link:http://www.mozilla.org/MPL/MPL-1.1.html[Mozilla Public License (Original)]
-* link:http://www.mozilla.org/MPL/MPL-1.1-annotated.html[Mozilla Public License (Annotated)]
-
-----
- MOZILLA PUBLIC LICENSE
- Version 1.1
-
- ---------------
-
-1. Definitions.
-
- 1.0.1. "Commercial Use" means distribution or otherwise making the
- Covered Code available to a third party.
-
- 1.1. "Contributor" means each entity that creates or contributes to
- the creation of Modifications.
-
- 1.2. "Contributor Version" means the combination of the Original
- Code, prior Modifications used by a Contributor, and the Modifications
- made by that particular Contributor.
-
- 1.3. "Covered Code" means the Original Code or Modifications or the
- combination of the Original Code and Modifications, in each case
- including portions thereof.
-
- 1.4. "Electronic Distribution Mechanism" means a mechanism generally
- accepted in the software development community for the electronic
- transfer of data.
-
- 1.5. "Executable" means Covered Code in any form other than Source
- Code.
-
- 1.6. "Initial Developer" means the individual or entity identified
- as the Initial Developer in the Source Code notice required by Exhibit
- A.
-
- 1.7. "Larger Work" means a work which combines Covered Code or
- portions thereof with code not governed by the terms of this License.
-
- 1.8. "License" means this document.
-
- 1.8.1. "Licensable" means having the right to grant, to the maximum
- extent possible, whether at the time of the initial grant or
- subsequently acquired, any and all of the rights conveyed herein.
-
- 1.9. "Modifications" means any addition to or deletion from the
- substance or structure of either the Original Code or any previous
- Modifications. When Covered Code is released as a series of files, a
- Modification is:
- A. Any addition to or deletion from the contents of a file
- containing Original Code or previous Modifications.
-
- B. Any new file that contains any part of the Original Code or
- previous Modifications.
-
- 1.10. "Original Code" means Source Code of computer software code
- which is described in the Source Code notice required by Exhibit A as
- Original Code, and which, at the time of its release under this
- License is not already Covered Code governed by this License.
-
- 1.10.1. "Patent Claims" means any patent claim(s), now owned or
- hereafter acquired, including without limitation, method, process,
- and apparatus claims, in any patent Licensable by grantor.
-
- 1.11. "Source Code" means the preferred form of the Covered Code for
- making modifications to it, including all modules it contains, plus
- any associated interface definition files, scripts used to control
- compilation and installation of an Executable, or source code
- differential comparisons against either the Original Code or another
- well known, available Covered Code of the Contributor's choice. The
- Source Code can be in a compressed or archival form, provided the
- appropriate decompression or de-archiving software is widely available
- for no charge.
-
- 1.12. "You" (or "Your") means an individual or a legal entity
- exercising rights under, and complying with all of the terms of, this
- License or a future version of this License issued under Section 6.1.
- For legal entities, "You" includes any entity which controls, is
- controlled by, or is under common control with You. For purposes of
- this definition, "control" means (a) the power, direct or indirect,
- to cause the direction or management of such entity, whether by
- contract or otherwise, or (b) ownership of more than fifty percent
- (50%) of the outstanding shares or beneficial ownership of such
- entity.
-
-2. Source Code License.
-
- 2.1. The Initial Developer Grant.
- The Initial Developer hereby grants You a world-wide, royalty-free,
- non-exclusive license, subject to third party intellectual property
- claims:
- (a) under intellectual property rights (other than patent or
- trademark) Licensable by Initial Developer to use, reproduce,
- modify, display, perform, sublicense and distribute the Original
- Code (or portions thereof) with or without Modifications, and/or
- as part of a Larger Work; and
-
- (b) under Patents Claims infringed by the making, using or
- selling of Original Code, to make, have made, use, practice,
- sell, and offer for sale, and/or otherwise dispose of the
- Original Code (or portions thereof).
-
- (c) the licenses granted in this Section 2.1(a) and (b) are
- effective on the date Initial Developer first distributes
- Original Code under the terms of this License.
-
- (d) Notwithstanding Section 2.1(b) above, no patent license is
- granted: 1) for code that You delete from the Original Code; 2)
- separate from the Original Code; or 3) for infringements caused
- by: i) the modification of the Original Code or ii) the
- combination of the Original Code with other software or devices.
-
- 2.2. Contributor Grant.
- Subject to third party intellectual property claims, each Contributor
- hereby grants You a world-wide, royalty-free, non-exclusive license
-
- (a) under intellectual property rights (other than patent or
- trademark) Licensable by Contributor, to use, reproduce, modify,
- display, perform, sublicense and distribute the Modifications
- created by such Contributor (or portions thereof) either on an
- unmodified basis, with other Modifications, as Covered Code
- and/or as part of a Larger Work; and
-
- (b) under Patent Claims infringed by the making, using, or
- selling of Modifications made by that Contributor either alone
- and/or in combination with its Contributor Version (or portions
- of such combination), to make, use, sell, offer for sale, have
- made, and/or otherwise dispose of: 1) Modifications made by that
- Contributor (or portions thereof); and 2) the combination of
- Modifications made by that Contributor with its Contributor
- Version (or portions of such combination).
-
- (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
- effective on the date Contributor first makes Commercial Use of
- the Covered Code.
-
- (d) Notwithstanding Section 2.2(b) above, no patent license is
- granted: 1) for any code that Contributor has deleted from the
- Contributor Version; 2) separate from the Contributor Version;
- 3) for infringements caused by: i) third party modifications of
- Contributor Version or ii) the combination of Modifications made
- by that Contributor with other software (except as part of the
- Contributor Version) or other devices; or 4) under Patent Claims
- infringed by Covered Code in the absence of Modifications made by
- that Contributor.
-
-3. Distribution Obligations.
-
- 3.1. Application of License.
- The Modifications which You create or to which You contribute are
- governed by the terms of this License, including without limitation
- Section 2.2. The Source Code version of Covered Code may be
- distributed only under the terms of this License or a future version
- of this License released under Section 6.1, and You must include a
- copy of this License with every copy of the Source Code You
- distribute. You may not offer or impose any terms on any Source Code
- version that alters or restricts the applicable version of this
- License or the recipients' rights hereunder. However, You may include
- an additional document offering the additional rights described in
- Section 3.5.
-
- 3.2. Availability of Source Code.
- Any Modification which You create or to which You contribute must be
- made available in Source Code form under the terms of this License
- either on the same media as an Executable version or via an accepted
- Electronic Distribution Mechanism to anyone to whom you made an
- Executable version available; and if made available via Electronic
- Distribution Mechanism, must remain available for at least twelve (12)
- months after the date it initially became available, or at least six
- (6) months after a subsequent version of that particular Modification
- has been made available to such recipients. You are responsible for
- ensuring that the Source Code version remains available even if the
- Electronic Distribution Mechanism is maintained by a third party.
-
- 3.3. Description of Modifications.
- You must cause all Covered Code to which You contribute to contain a
- file documenting the changes You made to create that Covered Code and
- the date of any change. You must include a prominent statement that
- the Modification is derived, directly or indirectly, from Original
- Code provided by the Initial Developer and including the name of the
- Initial Developer in (a) the Source Code, and (b) in any notice in an
- Executable version or related documentation in which You describe the
- origin or ownership of the Covered Code.
-
- 3.4. Intellectual Property Matters
- (a) Third Party Claims.
- If Contributor has knowledge that a license under a third party's
- intellectual property rights is required to exercise the rights
- granted by such Contributor under Sections 2.1 or 2.2,
- Contributor must include a text file with the Source Code
- distribution titled "LEGAL" which describes the claim and the
- party making the claim in sufficient detail that a recipient will
- know whom to contact. If Contributor obtains such knowledge after
- the Modification is made available as described in Section 3.2,
- Contributor shall promptly modify the LEGAL file in all copies
- Contributor makes available thereafter and shall take other steps
- (such as notifying appropriate mailing lists or newsgroups)
- reasonably calculated to inform those who received the Covered
- Code that new knowledge has been obtained.
-
- (b) Contributor APIs.
- If Contributor's Modifications include an application programming
- interface and Contributor has knowledge of patent licenses which
- are reasonably necessary to implement that API, Contributor must
- also include this information in the LEGAL file.
-
- (c) Representations.
- Contributor represents that, except as disclosed pursuant to
- Section 3.4(a) above, Contributor believes that Contributor's
- Modifications are Contributor's original creation(s) and/or
- Contributor has sufficient rights to grant the rights conveyed by
- this License.
-
- 3.5. Required Notices.
- You must duplicate the notice in Exhibit A in each file of the Source
- Code. If it is not possible to put such notice in a particular Source
- Code file due to its structure, then You must include such notice in a
- location (such as a relevant directory) where a user would be likely
- to look for such a notice. If You created one or more Modification(s)
- You may add your name as a Contributor to the notice described in
- Exhibit A. You must also duplicate this License in any documentation
- for the Source Code where You describe recipients' rights or ownership
- rights relating to Covered Code. You may choose to offer, and to
- charge a fee for, warranty, support, indemnity or liability
- obligations to one or more recipients of Covered Code. However, You
- may do so only on Your own behalf, and not on behalf of the Initial
- Developer or any Contributor. You must make it absolutely clear than
- any such warranty, support, indemnity or liability obligation is
- offered by You alone, and You hereby agree to indemnify the Initial
- Developer and every Contributor for any liability incurred by the
- Initial Developer or such Contributor as a result of warranty,
- support, indemnity or liability terms You offer.
-
- 3.6. Distribution of Executable Versions.
- You may distribute Covered Code in Executable form only if the
- requirements of Section 3.1-3.5 have been met for that Covered Code,
- and if You include a notice stating that the Source Code version of
- the Covered Code is available under the terms of this License,
- including a description of how and where You have fulfilled the
- obligations of Section 3.2. The notice must be conspicuously included
- in any notice in an Executable version, related documentation or
- collateral in which You describe recipients' rights relating to the
- Covered Code. You may distribute the Executable version of Covered
- Code or ownership rights under a license of Your choice, which may
- contain terms different from this License, provided that You are in
- compliance with the terms of this License and that the license for the
- Executable version does not attempt to limit or alter the recipient's
- rights in the Source Code version from the rights set forth in this
- License. If You distribute the Executable version under a different
- license You must make it absolutely clear that any terms which differ
- from this License are offered by You alone, not by the Initial
- Developer or any Contributor. You hereby agree to indemnify the
- Initial Developer and every Contributor for any liability incurred by
- the Initial Developer or such Contributor as a result of any such
- terms You offer.
-
- 3.7. Larger Works.
- You may create a Larger Work by combining Covered Code with other code
- not governed by the terms of this License and distribute the Larger
- Work as a single product. In such a case, You must make sure the
- requirements of this License are fulfilled for the Covered Code.
-
-4. Inability to Comply Due to Statute or Regulation.
-
- If it is impossible for You to comply with any of the terms of this
- License with respect to some or all of the Covered Code due to
- statute, judicial order, or regulation then You must: (a) comply with
- the terms of this License to the maximum extent possible; and (b)
- describe the limitations and the code they affect. Such description
- must be included in the LEGAL file described in Section 3.4 and must
- be included with all distributions of the Source Code. Except to the
- extent prohibited by statute or regulation, such description must be
- sufficiently detailed for a recipient of ordinary skill to be able to
- understand it.
-
-5. Application of this License.
-
- This License applies to code to which the Initial Developer has
- attached the notice in Exhibit A and to related Covered Code.
-
-6. Versions of the License.
-
- 6.1. New Versions.
- Netscape Communications Corporation ("Netscape") may publish revised
- and/or new versions of the License from time to time. Each version
- will be given a distinguishing version number.
-
- 6.2. Effect of New Versions.
- Once Covered Code has been published under a particular version of the
- License, You may always continue to use it under the terms of that
- version. You may also choose to use such Covered Code under the terms
- of any subsequent version of the License published by Netscape. No one
- other than Netscape has the right to modify the terms applicable to
- Covered Code created under this License.
-
- 6.3. Derivative Works.
- If You create or use a modified version of this License (which you may
- only do in order to apply it to code which is not already Covered Code
- governed by this License), You must (a) rename Your license so that
- the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
- "MPL", "NPL" or any confusingly similar phrase do not appear in your
- license (except to note that your license differs from this License)
- and (b) otherwise make it clear that Your version of the license
- contains terms which differ from the Mozilla Public License and
- Netscape Public License. (Filling in the name of the Initial
- Developer, Original Code or Contributor in the notice described in
- Exhibit A shall not of themselves be deemed to be modifications of
- this License.)
-
-7. DISCLAIMER OF WARRANTY.
-
- COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
- WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
- WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
- DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
- THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
- IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
- YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
- COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
- OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
- ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
-
-8. TERMINATION.
-
- 8.1. This License and the rights granted hereunder will terminate
- automatically if You fail to comply with terms herein and fail to cure
- such breach within 30 days of becoming aware of the breach. All
- sublicenses to the Covered Code which are properly granted shall
- survive any termination of this License. Provisions which, by their
- nature, must remain in effect beyond the termination of this License
- shall survive.
-
- 8.2. If You initiate litigation by asserting a patent infringement
- claim (excluding declatory judgment actions) against Initial Developer
- or a Contributor (the Initial Developer or Contributor against whom
- You file such action is referred to as "Participant") alleging that:
-
- (a) such Participant's Contributor Version directly or indirectly
- infringes any patent, then any and all rights granted by such
- Participant to You under Sections 2.1 and/or 2.2 of this License
- shall, upon 60 days notice from Participant terminate prospectively,
- unless if within 60 days after receipt of notice You either: (i)
- agree in writing to pay Participant a mutually agreeable reasonable
- royalty for Your past and future use of Modifications made by such
- Participant, or (ii) withdraw Your litigation claim with respect to
- the Contributor Version against such Participant. If within 60 days
- of notice, a reasonable royalty and payment arrangement are not
- mutually agreed upon in writing by the parties or the litigation claim
- is not withdrawn, the rights granted by Participant to You under
- Sections 2.1 and/or 2.2 automatically terminate at the expiration of
- the 60 day notice period specified above.
-
- (b) any software, hardware, or device, other than such Participant's
- Contributor Version, directly or indirectly infringes any patent, then
- any rights granted to You by such Participant under Sections 2.1(b)
- and 2.2(b) are revoked effective as of the date You first made, used,
- sold, distributed, or had made, Modifications made by that
- Participant.
-
- 8.3. If You assert a patent infringement claim against Participant
- alleging that such Participant's Contributor Version directly or
- indirectly infringes any patent where such claim is resolved (such as
- by license or settlement) prior to the initiation of patent
- infringement litigation, then the reasonable value of the licenses
- granted by such Participant under Sections 2.1 or 2.2 shall be taken
- into account in determining the amount or value of any payment or
- license.
-
- 8.4. In the event of termination under Sections 8.1 or 8.2 above,
- all end user license agreements (excluding distributors and resellers)
- which have been validly granted by You or any distributor hereunder
- prior to termination shall survive termination.
-
-9. LIMITATION OF LIABILITY.
-
- UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
- (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
- DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
- OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
- ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
- CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
- WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
- COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
- INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
- LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
- RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
- PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
- EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
- THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
-
-10. U.S. GOVERNMENT END USERS.
-
- The Covered Code is a "commercial item," as that term is defined in
- 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
- software" and "commercial computer software documentation," as such
- terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
- C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
- all U.S. Government End Users acquire Covered Code with only those
- rights set forth herein.
-
-11. MISCELLANEOUS.
-
- This License represents the complete agreement concerning subject
- matter hereof. If any provision of this License is held to be
- unenforceable, such provision shall be reformed only to the extent
- necessary to make it enforceable. This License shall be governed by
- California law provisions (except to the extent applicable law, if
- any, provides otherwise), excluding its conflict-of-law provisions.
- With respect to disputes in which at least one party is a citizen of,
- or an entity chartered or registered to do business in the United
- States of America, any litigation relating to this License shall be
- subject to the jurisdiction of the Federal Courts of the Northern
- District of California, with venue lying in Santa Clara County,
- California, with the losing party responsible for costs, including
- without limitation, court costs and reasonable attorneys' fees and
- expenses. The application of the United Nations Convention on
- Contracts for the International Sale of Goods is expressly excluded.
- Any law or regulation which provides that the language of a contract
- shall be construed against the drafter shall not apply to this
- License.
-
-12. RESPONSIBILITY FOR CLAIMS.
-
- As between Initial Developer and the Contributors, each party is
- responsible for claims and damages arising, directly or indirectly,
- out of its utilization of rights under this License and You agree to
- work with Initial Developer and Contributors to distribute such
- responsibility on an equitable basis. Nothing herein is intended or
- shall be deemed to constitute any admission of liability.
-
-13. MULTIPLE-LICENSED CODE.
-
- Initial Developer may designate portions of the Covered Code as
- "Multiple-Licensed". "Multiple-Licensed" means that the Initial
- Developer permits you to utilize portions of the Covered Code under
- Your choice of the NPL or the alternative licenses, if any, specified
- by the Initial Developer in the file described in Exhibit A.
-
-EXHIBIT A -Mozilla Public License.
-
- ``The contents of this file are subject to the Mozilla Public License
- Version 1.1 (the "License"); you may not use this file except in
- compliance with the License. You may obtain a copy of the License at
- http://www.mozilla.org/MPL/
-
- Software distributed under the License is distributed on an "AS IS"
- basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
- License for the specific language governing rights and limitations
- under the License.
-
- The Original Code is ______________________________________.
-
- The Initial Developer of the Original Code is ________________________.
- Portions created by ______________________ are Copyright (C) ______
- _______________________. All Rights Reserved.
-
- Contributor(s): ______________________________________.
-
- Alternatively, the contents of this file may be used under the terms
- of the _____ license (the "[___] License"), in which case the
- provisions of [______] License are applicable instead of those
- above. If you wish to allow use of your version of this file only
- under the terms of the [____] License and not to allow others to use
- your version of this file under the MPL, indicate your decision by
- deleting the provisions above and replace them with the notice and
- other provisions required by the [___] License. If you do not delete
- the provisions above, a recipient may use your version of this file
- under either the MPL or the [___] License."
-
- [NOTE: The text of this Exhibit A may differ slightly from the text of
- the notices in the Source Code files of the Original Code. You should
- use the text of this Exhibit A rather than the text found in the
- Original Code Source Code for Your Modifications.]
-----
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/man/Makefile b/Documentation/man/Makefile
index 75e6533..945f215 100644
--- a/Documentation/man/Makefile
+++ b/Documentation/man/Makefile
@@ -34,6 +34,7 @@
cmd-rename-group.txt \
cmd-review.txt \
cmd-set-account.txt \
+cmd-set-members.txt \
cmd-set-project-parent.txt \
cmd-set-project.txt \
cmd-set-reviewers.txt \
diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt
index 3ffcb40..ce88ab7 100644
--- a/Documentation/pgm-daemon.txt
+++ b/Documentation/pgm-daemon.txt
@@ -15,6 +15,7 @@
[\--console-log]
[\--slave]
[\--headless]
+ [\--init]
DESCRIPTION
-----------
@@ -63,6 +64,10 @@
Don't start the default Gerrit UI. May be useful when Gerrit is
run with an alternative UI.
+\--init::
+ Run init before starting the daemon. This will create a new site or
+ upgrade an existing site.
+
CONTEXT
-------
This command can only be run on a server which has direct
diff --git a/Documentation/pgm-index.txt b/Documentation/pgm-index.txt
index 7a1edcf..987b4ac 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -23,6 +23,9 @@
link:pgm-prolog-shell.html[prolog-shell]::
Simple interactive Prolog interpreter.
+link:pgm-reindex.html[reindex]::
+ Rebuild the secondary index.
+
link:pgm-rulec.html[rulec]::
Compile project-specific Prolog rules to JARs.
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index 57decdd..3d6cb73 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -12,6 +12,8 @@
-d <SITE_PATH>
[\--batch]
[\--no-auto-start]
+ [\--list-plugins]
+ [\--install-plugin=<PLUGIN_NAME>]
DESCRIPTION
-----------
@@ -45,6 +47,12 @@
Location of the gerrit.config file, and all other per-site
configuration data, supporting libraries and log files.
+\--list-plugins::
+ Print names of plugins that can be installed during init process.
+
+\--install-plugin:
+ Automatically install plugin with given name without asking.
+
CONTEXT
-------
This command can only be run on a server which has direct
diff --git a/Documentation/pgm-prolog-shell.txt b/Documentation/pgm-prolog-shell.txt
index f3fa2d8..3189e90 100644
--- a/Documentation/pgm-prolog-shell.txt
+++ b/Documentation/pgm-prolog-shell.txt
@@ -20,7 +20,7 @@
-s::
Dynamically load the Prolog source code at startup,
as though the user had entered `['FILE.pl'].` into
- the interepter once it was running. This option may
+ the interpreter once it was running. This option may
be supplied more than once to load multiple files.
EXAMPLES
diff --git a/Documentation/pgm-reindex.txt b/Documentation/pgm-reindex.txt
new file mode 100644
index 0000000..2b44f6b
--- /dev/null
+++ b/Documentation/pgm-reindex.txt
@@ -0,0 +1,41 @@
+reindex
+=======
+
+NAME
+----
+reindex - Rebuild the secondary index
+
+SYNOPSIS
+--------
+[verse]
+'java' -jar gerrit.war 'reindex' [<OPTIONS>]
+
+DESCRIPTION
+-----------
+Rebuilds the secondary index.
+
+OPTIONS
+-------
+--threads::
+ Number of threads to use for indexing.
+
+--schema-version::
+ Schema version to reindex; default is most recent version.
+
+--output::
+ Prefix for output; path for local disk index, or prefix for remote index.
+
+--verbose::
+ Output debug information for each change.
+
+--dry-run::
+ Dry run. Don't write anything to index.
+
+CONTEXT
+-------
+The secondary index must be enabled. See
+link:config-gerrit.html#index.type[index.type].
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index 170d11a..b1710a2 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -182,7 +182,7 @@
<2> label `'Verified'` is rejected. Change is not submittable.
<3> label `'Author-is-John-Doe'` is needed for the change to become submittable.
Note that this tells nothing about how this criteria will be met. It is up
- to the implementor of the `submit_rule` to return `label('Author-is-John-Doe',
+ to the implementer of the `submit_rule` to return `label('Author-is-John-Doe',
ok(_))` when this criteria is met. Most likely, it will have to match
against `gerrit:commit_author` in order to check if this criteria is met.
This will become clear through the examples below.
@@ -645,7 +645,7 @@
Reusing the default submit policy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-To get results of Gerrits default submit policy we use the
+To get results of Gerrit's default submit policy we use the
`gerrit:default_submit` predicate. The `gerrit:default_submit(X)` includes all
categories from the database. This means that if we write a submit rule like:
@@ -743,7 +743,7 @@
The latter implementation is probably easier to understand and the code looks
cleaner. Note, however, that the latter implementation will always return the
two standard categories only (`Code-Review` and `Verified`) even if a new
-category has beeen inserted into the database. To include the new category
+category has been inserted into the database. To include the new category
the `rules.pl` would need to be modified or a `submit_filter` in a parent
project would have to care about including the new category in the result
of this `submit_rule`.
diff --git a/Documentation/replace_macros.py b/Documentation/replace_macros.py
new file mode 100755
index 0000000..076cb49
--- /dev/null
+++ b/Documentation/replace_macros.py
@@ -0,0 +1,87 @@
+#!/usr/bin/python
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from optparse import OptionParser
+import os
+import re
+import sys
+
+PAT_GERRIT = re.compile('^GERRIT')
+PAT_INCLUDE = re.compile('^(include::.*)(\[\])$')
+PAT_GET = re.compile('^get::([^ \t\n]*)')
+
+GERRIT_UPLINK = """
+
+++++
+<hr style=\"
+ height: 2px;
+ color: silver;
+ margin-top: 1.2em;
+ margin-bottom: 0.5em;
+\">
+++++
+"""
+
+GET_MACRO = """
+
+++++
+<a id=\"{0}\" onmousedown="javascript:
+ var i = document.URL.lastIndexOf(\'/Documentation/\');
+ var url = document.URL.substring(0, i) + \'{0}\';
+ document.getElementById(\'{0}\').href = url;">
+ GET {0} HTTP/1.0
+</a>
+++++
+"""
+
+opts = OptionParser()
+opts.add_option('-o', '--out', help='output file')
+opts.add_option('-s', '--src', help='source file')
+opts.add_option('-x', '--suffix', help='suffix for included filenames')
+options, _ = opts.parse_args()
+
+try:
+ out_file = open(options.out, 'w')
+ src_file = open(options.src, 'r')
+ last_line = ''
+ ignore_next_line = False
+ for line in src_file.xreadlines():
+ if PAT_GERRIT.match(last_line):
+ # Case of "GERRIT\n------" at the footer
+ out_file.write(GERRIT_UPLINK)
+ last_line = ''
+ elif PAT_INCLUDE.match(line):
+ # Case of 'include::<filename>'
+ match = PAT_INCLUDE.match(line)
+ out_file.write(last_line)
+ last_line = match.group(1) + options.suffix + match.group(2) + '\n'
+ elif PAT_GET.match(line):
+ # Case of '****\nget::<url>\n****' in rest api
+ url = PAT_GET.match(line).group(1)
+ out_file.write(GET_MACRO.format(url))
+ ignore_next_line = True
+ elif ignore_next_line:
+ # Handle the trailing '****' of the 'get::' case
+ last_line = ''
+ ignore_next_line = False
+ else:
+ out_file.write(last_line)
+ last_line = line
+ out_file.write(last_line)
+ out_file.close()
+except IOError as err:
+ sys.stderr.write(
+ "error while expanding %s to %s: %s" % (options.src, options.out, err))
+ exit(1)
diff --git a/Documentation/rest-api-access.txt b/Documentation/rest-api-access.txt
new file mode 100644
index 0000000..6c786e4
--- /dev/null
+++ b/Documentation/rest-api-access.txt
@@ -0,0 +1,380 @@
+Gerrit Code Review - /access/ REST API
+======================================
+
+This page describes the access rights related REST endpoints.
+Please also take note of the general information on the
+link:rest-api.html[REST API].
+
+[[access-endpoints]]
+Access Rights Endpoints
+-----------------------
+
+[[list-access]]
+List Access Rights
+~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /access/?project=link:rest-api-projects.html#project-name[\{project-name\}]'
+
+Lists the access rights for projects. The projects for which the access
+rights should be returned must be specified as `project` options. The
+`project` can be specified multiple times.
+
+As result a map is returned that maps the project name to
+link:#project-access-info[ProjectAccessInfo] entities.
+
+The entries in the map are sorted by project name.
+
+.Request
+----
+ GET /access/?project=MyProject&project=All-Projects HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "All-Projects": {
+ "revision": "edd453d18e08640e67a8c9a150cec998ed0ac9aa",
+ "local": {
+ "GLOBAL_CAPABILITIES": {
+ "permissions": {
+ "priority": {
+ "rules": {
+ "15bfcd8a6de1a69c50b30cedcdcc951c15703152": {
+ "action": "BATCH"
+ }
+ }
+ },
+ "streamEvents": {
+ "rules": {
+ "15bfcd8a6de1a69c50b30cedcdcc951c15703152": {
+ "action": "ALLOW"
+ }
+ }
+ },
+ "administrateServer": {
+ "rules": {
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW"
+ }
+ }
+ }
+ }
+ },
+ "refs/meta/config": {
+ "permissions": {
+ "submit": {
+ "rules": {
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW"
+ },
+ "global:Project-Owners": {
+ "action": "ALLOW"
+ }
+ }
+ },
+ "label-Code-Review": {
+ "label": "Code-Review",
+ "rules": {
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW",
+ "min": -2,
+ "max": 2
+ },
+ "global:Project-Owners": {
+ "action": "ALLOW",
+ "min": -2,
+ "max": 2
+ }
+ }
+ },
+ "read": {
+ "exclusive": true,
+ "rules": {
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW"
+ },
+ "global:Project-Owners": {
+ "action": "ALLOW"
+ }
+ }
+ },
+ "push": {
+ "rules": {
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW"
+ },
+ "global:Project-Owners": {
+ "action": "ALLOW"
+ }
+ }
+ }
+ }
+ },
+ "refs/for/refs/*": {
+ "permissions": {
+ "pushMerge": {
+ "rules": {
+ "global:Registered-Users": {
+ "action": "ALLOW"
+ }
+ }
+ },
+ "push": {
+ "rules": {
+ "global:Registered-Users": {
+ "action": "ALLOW"
+ }
+ }
+ }
+ }
+ },
+ "refs/tags/*": {
+ "permissions": {
+ "pushSignedTag": {
+ "rules": {
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW"
+ },
+ "global:Project-Owners": {
+ "action": "ALLOW"
+ }
+ }
+ },
+ "pushTag": {
+ "rules": {
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW"
+ },
+ "global:Project-Owners": {
+ "action": "ALLOW"
+ }
+ }
+ }
+ }
+ },
+ "refs/heads/*": {
+ "permissions": {
+ "forgeCommitter": {
+ "rules": {
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW"
+ },
+ "global:Project-Owners": {
+ "action": "ALLOW"
+ }
+ }
+ },
+ "forgeAuthor": {
+ "rules": {
+ "global:Registered-Users": {
+ "action": "ALLOW"
+ }
+ }
+ },
+ "submit": {
+ "rules": {
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW"
+ },
+ "global:Project-Owners": {
+ "action": "ALLOW"
+ }
+ }
+ },
+ "editTopicName": {
+ "rules": {
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW",
+ "force": true
+ },
+ "global:Project-Owners": {
+ "action": "ALLOW",
+ "force": true
+ }
+ }
+ },
+ "label-Code-Review": {
+ "label": "Code-Review",
+ "rules": {
+ "global:Registered-Users": {
+ "action": "ALLOW",
+ "min": -1,
+ "max": 1
+ },
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW",
+ "min": -2,
+ "max": 2
+ },
+ "global:Project-Owners": {
+ "action": "ALLOW",
+ "min": -2,
+ "max": 2
+ }
+ }
+ },
+ "create": {
+ "rules": {
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW"
+ },
+ "global:Project-Owners": {
+ "action": "ALLOW"
+ }
+ }
+ },
+ "push": {
+ "rules": {
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW"
+ },
+ "global:Project-Owners": {
+ "action": "ALLOW"
+ }
+ }
+ }
+ }
+ },
+ "refs/*": {
+ "permissions": {
+ "read": {
+ "rules": {
+ "global:Anonymous-Users": {
+ "action": "ALLOW"
+ },
+ "53a4f647a89ea57992571187d8025f830625192a": {
+ "action": "ALLOW"
+ }
+ }
+ }
+ }
+ }
+ },
+ "is_owner": true,
+ "owner_of": [
+ "GLOBAL_CAPABILITIES",
+ "refs/meta/config",
+ "refs/for/refs/*",
+ "refs/tags/*",
+ "refs/heads/*",
+ "refs/*"
+ ],
+ "can_upload": true,
+ "can_add": true,
+ "config_visible": true
+ },
+ "MyProject": {
+ "revision": "61157ed63e14d261b6dca40650472a9b0bd88474",
+ "inherits_from": {
+ "kind": "gerritcodereview#project",
+ "id": "All-Projects",
+ "name": "All-Projects",
+ "description": "Access inherited by all other projects."
+ },
+ "local": {},
+ "is_owner": true,
+ "owner_of": [
+ "refs/*"
+ ],
+ "can_upload": true,
+ "can_add": true,
+ "config_visible": true
+ }
+ }
+----
+
+[[access-section-info]]
+AccessSectionInfo
+~~~~~~~~~~~~~~~~~
+The `AccessSectionInfo` describes the access rights that are assigned
+on a ref.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name ||Description
+|`permissions` ||
+The permissions assigned on the ref of this access section as a map
+that maps the permission names to link:#permission-info[PermissionInfo]
+entities.
+|==================================
+
+[[permission-info]]
+PermissionInfo
+~~~~~~~~~~~~~~
+The `PermissionInfo` entity contains information about an assigned
+permission.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name ||Description
+|`label` |optional|
+The name of the label. Not set if it's not a label permission.
+|`exclusive` |not set if `false`|
+Whether this permission is assigned exclusively.
+|`rules` ||
+The rules assigned for this permission as a map that maps the UUIDs of
+the groups for which the permission are assigned to
+link:#permission-info[PermissionRuleInfo] entities.
+|==================================
+
+[[permission-rule-info]]
+PermissionRuleInfo
+~~~~~~~~~~~~~~~~~~
+The `PermissionRuleInfo` entity contains information about a permission
+rule that is assigned to group.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name ||Description
+|`action` ||
+The action of this rule. For normal permissions this can be `ALLOW`,
+`DENY` or `BLOCK`. Special values for global capabilities are
+`INTERACTIVE` and `BATCH`.
+|`force` |not set if `false`|
+Whether the force flag is set.
+|`min` |
+not set if range if empty (from `0` to `0`) or not set|
+The min value of the permission range.
+|`max` |
+not set if range if empty (from `0` to `0`) or not set|
+The max value of the permission range.
+|==================================
+
+[[project-access-info]]
+ProjectAccessInfo
+~~~~~~~~~~~~~~~~~
+The `ProjectAccessInfo` entity contains information about the access
+rights for a project.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name ||Description
+|`revision` ||
+The revision of the `refs/meta/config` branch from which the access
+rights were loaded.
+|`inherits_from` |not set for the `All-Project` project|
+The parent project from which permissions are inherited as a
+link:rest-api-projects.html#project-info[ProjectInfo] entity.
+|`local` ||
+The local access rights of the project as a map that maps the refs to
+link:#access-section-info[AccessSectionInfo] entities.
+|`is_owner` |not set if `false`|
+Whether the calling user owns this project.
+|`owner_of` ||The list of refs owned by the calling user.
+|`can_upload` |not set if `false`|
+Whether the calling user can upload to any ref.
+|`can_add` |not set if `false`|
+Whether the calling user can add any ref.
+|`config_visible` |not set if `false`|
+Whether the calling user can see the `refs/meta/config` branch of the
+project.
+|==================================
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index f17a741..7db83a4 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -5,8 +5,9 @@
Please also take note of the general information on the
link:rest-api.html[REST API].
-Endpoints
----------
+[[account-endpoints]]
+Account Endpoints
+-----------------
[[get-account]]
Get Account
@@ -31,10 +32,565 @@
{
"_account_id": 1000096,
"name": "John Doe",
+ "email": "john.doe@example.com",
+ "username": "john"
+ }
+----
+
+[[create-account]]
+Create Account
+~~~~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#username[\{username\}]'
+
+Creates a new account.
+
+In the request body additional data for the account can be provided as
+link:#account-input[AccountInput].
+
+.Request
+----
+ PUT /accounts/john HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "ssh_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw==",
+ "http_password": "19D9aIn7zePb",
+ "groups": [
+ "MyProject-Owners"
+ ]
+ }
+----
+
+As response a detailed link:#account-info[AccountInfo] entity is
+returned that describes the created account.
+
+.Response
+----
+ HTTP/1.1 201 Created
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "_account_id": 1000195,
+ "name": "John Doe",
"email": "john.doe@example.com"
}
----
+[[get-account-name]]
+Get Account Name
+~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/name'
+
+Retrieves the full name of an account.
+
+.Request
+----
+ GET /accounts/self/name HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ "John Doe"
+----
+
+If the account does not have a name an empty string is returned.
+
+[[set-account-name]]
+Set Account Name
+~~~~~~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#account-id[\{account-id\}]/name'
+
+Sets the full name of an account.
+
+The new account name must be provided in the request body inside
+a link:#account-name-input[AccountNameInput] entity.
+
+.Request
+----
+ PUT /accounts/self/name HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "name": "John F. Doe"
+ }
+----
+
+As response the new account name is returned.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ "John F. Doe"
+----
+
+If the name was deleted the response is "`204 No Content`".
+
+Some realms may not allow to modify the account name. In this case the
+request is rejected with "`405 Method Not Allowed`".
+
+[[delete-account-name]]
+Delete Account Name
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /accounts/link:#account-id[\{account-id\}]/name'
+
+Deletes the name of an account.
+
+.Request
+----
+ DELETE /accounts/self/name HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
+[[get-username]]
+Get Username
+~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/username'
+
+Retrieves the username of an account.
+
+.Request
+----
+ GET /accounts/self/username HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ "john.doe"
+----
+
+If the account does not have a username the response is `404 Not Found`.
+
+[[get-active]]
+Get Active
+~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/active'
+
+Checks if an account is active.
+
+.Request
+----
+ GET /accounts/john.doe@example.com/active HTTP/1.0
+----
+
+As response `200 OK` is returned for an active account and
+`404 Not Found` is returned for an inactive account.
+
+.Response
+----
+ HTTP/1.1 200 OK
+----
+
+[[set-active]]
+Set Active
+~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#account-id[\{account-id\}]/active'
+
+Sets the account state to active.
+
+.Request
+----
+ PUT /accounts/john.doe@example.com/active HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 201 Created
+----
+
+If the account was already active the response is `200 OK`.
+
+[[delete-active]]
+Delete Active
+~~~~~~~~~~~~~
+[verse]
+'DELETE /accounts/link:#account-id[\{account-id\}]/active'
+
+Sets the account state to inactive.
+
+.Request
+----
+ DELETE /accounts/john.doe@example.com/active HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
+If the account was already inactive the response is `404 Not Found`.
+
+[[get-http-password]]
+Get HTTP Password
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/password.http'
+
+Retrieves the HTTP password of an account.
+
+.Request
+----
+ GET /accounts/john.doe@example.com/password.http HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ "ETxgpih8xrNs"
+----
+
+If the account does not have an HTTP password the response is `404 Not Found`.
+
+[[set-http-password]]
+Set/Generate HTTP Password
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#account-id[\{account-id\}]/password.http'
+
+Sets/Generates the HTTP password of an account.
+
+The options for setting/generating the HTTP password must be provided
+in the request body inside a link:#http-password-input[
+HttpPasswordInput] entity.
+
+.Request
+----
+ PUT /accounts/self/password.http HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "generate": true
+ }
+----
+
+As response the new HTTP password is returned.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ "ETxgpih8xrNs"
+----
+
+If the HTTP password was deleted the response is "`204 No Content`".
+
+[[delete-http-password]]
+Delete HTTP Password
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /accounts/link:#account-id[\{account-id\}]/password.http'
+
+Deletes the HTTP password of an account.
+
+.Request
+----
+ DELETE /accounts/self/password.http HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
+[[list-account-emails]]
+List Account Emails
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/emails'
+
+Returns the email addresses that are configured for the specified user.
+
+.Request
+----
+ GET /accounts/self/emails HTTP/1.0
+----
+
+As response the email addresses of the user are returned as a list of
+link:#email-info[EmailInfo] entities.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ [
+ {
+ "email": "john.doe@example.com",
+ "preferred": true
+ },
+ {
+ "email": "j.doe@example.com"
+ }
+ ]
+----
+
+[[get-account-email]]
+Get Account Email
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/emails/link:#email-id[\{email-id\}]'
+
+Retrieves an email address of a user.
+
+.Request
+----
+ GET /accounts/self/emails/john.doe@example.com HTTP/1.0
+----
+
+As response an link:#email-info[EmailInfo] entity is returned that
+describes the email address.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "email": "john.doe@example.com",
+ "preferred": true
+ }
+----
+
+[[create-account-email]]
+Create Account Email
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#account-id[\{account-id\}]/emails/link:#email-id[\{email-id\}]'
+
+Registers a new email address for the user. A verification email is
+sent with a link that needs to be visited to confirm the email address,
+unless `DEVELOPMENT_BECOME_ANY_ACCOUNT` is used as authentication type.
+For the development mode email addresses are directly added without
+confirmation. A Gerrit administrator may add an email address without
+confirmation by setting `no_confirmation` in the
+link:#email-input[EmailInput].
+
+In the request body additional data for the email address can be
+provided as link:#email-input[EmailInput].
+
+.Request
+----
+ PUT /accounts/self/emails/john.doe@example.com HTTP/1.0
+----
+
+As response the new email address is returned as
+link:#email-info[EmailInfo] entity.
+
+.Response
+----
+ HTTP/1.1 201 Created
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "email": "john.doe@example.com",
+ "pending_confirmation": true
+ }
+----
+
+[[delete-account-email]]
+Delete Account Email
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /accounts/link:#account-id[\{account-id\}]/emails/link:#email-id[\{email-id\}]'
+
+Deletes an email address of an account.
+
+.Request
+----
+ DELETE /accounts/self/emails/john.doe@example.com HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
+[[set-preferred-email]]
+Set Preferred Email
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#account-id[\{account-id\}]/emails/link:#email-id[\{email-id\}]/preferred'
+
+Sets an email address as preferred email address for an account.
+
+.Request
+----
+ PUT /accounts/self/emails/john.doe@example.com/preferred HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 201 Created
+----
+
+If the email address was already the preferred email address of the
+account the response is "`200 OK`".
+
+[[list-ssh-keys]]
+List SSH Keys
+~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/sshkeys'
+
+Returns the SSH keys of an account.
+
+.Request
+----
+ GET /accounts/self/sshkeys HTTP/1.0
+----
+
+As response the SSH keys of the account are returned as a list of
+link:#ssh-key-info[SshKeyInfo] entities.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ [
+ {
+ "seq": 1,
+ "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d john.doe@example.com",
+ "encoded_key": "AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d",
+ "algorithm": "ssh-rsa",
+ "comment": "john.doe@example.com",
+ "valid": true
+ }
+ ]
+----
+
+[[get-ssh-key]]
+Get SSH Key
+~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/sshkeys/link:#ssh-key-id[\{ssh-key-id\}]'
+
+Retrieves an SSH key of a user.
+
+.Request
+----
+ GET /accounts/self/sshkeys/1 HTTP/1.0
+----
+
+As response an link:#ssh-key-info[SshKeyInfo] entity is returned that
+describes the SSH key.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "seq": 1,
+ "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d john.doe@example.com",
+ "encoded_key": "AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d",
+ "algorithm": "ssh-rsa",
+ "comment": "john.doe@example.com",
+ "valid": true
+ }
+----
+
+[[add-ssh-key]]
+Add SSH Key
+~~~~~~~~~~~
+[verse]
+'POST /accounts/link:#account-id[\{account-id\}]/sshkeys'
+
+Adds an SSH key for a user.
+
+The SSH public key must be provided as raw content in the request body.
+
+.Request
+----
+ POST /accounts/self/sshkeys HTTP/1.0
+ Content-Type: plain/text
+
+ AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d
+----
+
+As response an link:#ssh-key-info[SshKeyInfo] entity is returned that
+describes the new SSH key.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "seq": 2,
+ "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d john.doe@example.com",
+ "encoded_key": "AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw\u003d\u003d",
+ "algorithm": "ssh-rsa",
+ "comment": "john.doe@example.com",
+ "valid": true
+ }
+----
+
+[[delete-ssh-key]]
+Delete SSH Key
+~~~~~~~~~~~~~~
+[verse]
+'DELETE /accounts/link:#account-id[\{account-id\}]/sshkeys/link:#ssh-key-id[\{ssh-key-id\}]'
+
+Deletes an SSH key of a user.
+
+.Request
+----
+ DELETE /accounts/self/sshkeys/2 HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
[[list-account-capabilities]]
List Account Capabilities
~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -103,8 +659,7 @@
"flushCaches": true,
"viewConnections": true,
"viewQueue": true,
- "runGC": true,
- "startReplication": true
+ "runGC": true
}
----
@@ -391,6 +946,22 @@
Identifier of a global capability. Valid values are all field names of
the link:#capability-info[CapabilityInfo] entity.
+[[email-id]]
+\{email-id\}
+~~~~~~~~~~~~
+An email address, or `preferred` for the preferred email address of the
+user.
+
+[[username]]
+\{username\}
+~~~~~~~~~~~~
+The user name.
+
+[[ssh-key-id]]
+\{ssh-key-id\}
+~~~~~~~~~~~~~~
+The sequence number of the SSH key.
+
[[json-entities]]
JSON Entities
@@ -410,8 +981,43 @@
|`email` |optional|
The email address the user prefers to be contacted through. +
Only set if detailed account information is requested.
+|`username` |optional|The username of the user. +
+Only set if detailed account information is requested.
|===========================
+[[account-input]]
+AccountInput
+~~~~~~~~~~~~
+The `AccountInput` entity contains information for the creation of
+a new account.
+
+[options="header",width="50%",cols="1,^2,4"]
+|============================
+|Field Name ||Description
+|`username` |optional|
+The user name. If provided, must match the user name from the URL.
+|`name` |optional|The full name of the user.
+|`email` |optional|The email address of the user.
+|`ssh_key` |optional|The public SSH key of the user.
+|`http_password`|optional|The HTTP password of the user.
+|`groups` |optional|
+A list of link:rest-api-groups.html#group-id[group IDs] that identify
+the groups to which the user should be added.
+|============================
+
+[[account-name-input]]
+AccountNameInput
+~~~~~~~~~~~~~~~~
+The `AccountNameInput` entity contains information for setting a name
+for an account.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=============================
+|Field Name ||Description
+|`name` |optional|The new full name of the account. +
+If not set or if set to an empty string, the account name is deleted.
+|=============================
+
[[capability-info]]
CapabilityInfo
~~~~~~~~~~~~~~
@@ -453,9 +1059,6 @@
|`runGC` |not set if `false`|Whether the user has the
link:access-control.html#capability_runGC[Run Garbage Collection]
capability.
-|`startReplication` |not set if `false`|Whether the user has the
-link:access-control.html#capability_startReplication[Start Replication]
-capability.
|=================================
[[diff-preferences-info]]
@@ -551,6 +1154,65 @@
Number of spaces that should be used to display one tab.
|=====================================
+[[email-info]]
+EmailInfo
+~~~~~~~~~
+The `EmailInfo` entity contains information about an email address of a
+user.
+
+[options="header",width="50%",cols="1,^1,5"]
+|========================
+|Field Name ||Description
+|`email` ||The email address.
+|`preferred`|not set if `false`|
+Whether this is the preferred email address of the user.
+|`pending_confirmation`|not set if `false`|
+Set true if the user must confirm control of the email address
+by following a verification link before Gerrit will permit use of
+this address.
+|========================
+
+[[email-input]]
+EmailInput
+~~~~~~~~~~
+The `EmailInput` entity contains information for registering a new
+email address.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==============================
+|Field Name ||Description
+|`email` ||
+The email address. If provided, must match the email address from the
+URL.
+|`preferred` |`false` if not set|
+Whether the new email address should become the preferred email address
+of the user (only supported if `no_confirmation` is set or if the
+authentication type is `DEVELOPMENT_BECOME_ANY_ACCOUNT`).
+|`no_confirmation`|`false` if not set|
+Whether the email address should be added without confirmation. In this
+case no verification email is sent to the user. +
+Only Gerrit administrators are allowed to add email addresses without
+confirmation.
+|==============================
+
+[[http-password-input]]
+HttpPasswordInput
+~~~~~~~~~~~~~~~~~
+The `HttpPasswordInput` entity contains information for setting/generating
+an HTTP password.
+
+[options="header",width="50%",cols="1,^1,5"]
+|============================
+|Field Name ||Description
+|`generate` |`false` if not set|
+Whether a new HTTP password should be generated
+|`http_password`|optional|
+The new HTTP password. Only Gerrit administrators may set the HTTP
+password directly. +
+If empty or not set and `generate` is false or not set, the HTTP
+password is deleted.
+|============================
+
[[query-limit-info]]
QueryLimitInfo
~~~~~~~~~~~~~~
@@ -564,6 +1226,23 @@
|`max` |Upper limit.
|================================
+[[ssh-key-info]]
+SshKeyInfo
+~~~~~~~~~~
+The `SshKeyInfo` entity contains information about an SSH key of a
+user.
+
+[options="header",width="50%",cols="1,^1,5"]
+|=============================
+|Field Name ||Description
+|`seq` ||The sequence number of the SSH key.
+|`ssh_public_key`||The complete public SSH key.
+|`encoded_key` ||The encoded key.
+|`algorithm` ||The algorithm of the SSH key.
+|`comment` |optional|The comment of the SSH key.
+|`valid` ||Whether the SSH key is valid.
+|=============================
+
GERRIT
------
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 09c7e11..0e0a355 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -48,7 +48,6 @@
"status": "NEW",
"created": "2012-07-17 07:18:30.854000000",
"updated": "2012-07-17 07:19:27.766000000",
- "reviewed": true,
"mergeable": true,
"_sortkey": "001e7057000006dc",
"_number": 1756,
@@ -120,7 +119,6 @@
"status": "NEW",
"created": "2012-07-17 07:18:30.854000000",
"updated": "2012-07-17 07:19:27.766000000",
- "reviewed": true,
"mergeable": true,
"_sortkey": "001e7057000006dc",
"_number": 1756,
@@ -213,6 +211,19 @@
* `MESSAGES`: include messages associated with the change.
--
+[[actions]]
+--
+* `CURRENT_ACTIONS`: include information on available actions
+ for the change and its current revision. The caller must be
+ authenticated to obtain the available actions.
+--
+
+[[reviewed]]
+--
+* `REVIEWED`: include the `reviewed` field if the caller is
+ authenticated and has commented on the current revision.
+--
+
.Request
----
GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES HTTP/1.0
@@ -344,7 +355,6 @@
"status": "NEW",
"created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000",
- "reviewed": true,
"mergeable": true,
"_sortkey": "0023412400000f7d",
"_number": 3965,
@@ -364,6 +374,11 @@
detailed labels], link:#detailed-accounts[detailed accounts], and
link:#messages[messages].
+Additional fields can be obtained by adding `o` parameters, each
+option requires more database lookups and slows down the query
+response time to the client so they are generally disabled by
+default. Fields are described in link:#list-changes[Query Changes].
+
.Request
----
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/detail HTTP/1.0
@@ -389,7 +404,6 @@
"status": "NEW",
"created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000",
- "reviewed": true,
"mergeable": true,
"_sortkey": "0023412400000f7d",
"_number": 3965,
@@ -628,7 +642,6 @@
"status": "ABANDONED",
"created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000",
- "reviewed": true,
"mergeable": true,
"_sortkey": "0023412400000f7d",
"_number": 3965,
@@ -687,7 +700,6 @@
"status": "NEW",
"created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000",
- "reviewed": true,
"mergeable": true,
"_sortkey": "0023412400000f7d",
"_number": 3965,
@@ -710,6 +722,95 @@
change is new
----
+[[rebase-change]]
+Rebase Change
+~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/rebase'
+
+Rebases a change.
+
+.Request
+----
+ POST /changes/myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2/rebase HTTP/1.0
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the rebased change. Information about the current patch set
+is included.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#change",
+ "id": "myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
+ "project": "myProject",
+ "branch": "master",
+ "change_id": "I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
+ "subject": "Implement Feature X",
+ "status": "NEW",
+ "created": "2013-02-01 09:59:32.126000000",
+ "updated": "2013-02-21 11:16:36.775000000",
+ "mergeable": false,
+ "_sortkey": "0024cf9a000012bf",
+ "_number": 4799,
+ "owner": {
+ "name": "John Doe"
+ },
+ "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
+ "revisions": {
+ "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
+ "_number": 2,
+ "fetch": {
+ "http": {
+ "url": "http://gerrit:8080/myProject",
+ "ref": "refs/changes/99/4799/2"
+ }
+ },
+ "commit": {
+ "parents": [
+ {
+ "commit": "b4003890dadd406d80222bf1ad8aca09a4876b70",
+ "subject": "Implement Feature A"
+ }
+ ],
+ "author": {
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "date": "2013-05-07 15:21:27.000000000",
+ "tz": 120
+ },
+ "committer": {
+ "name": "Gerrit Code Review",
+ "email": "gerrit-server@example.com",
+ "date": "2013-05-07 15:35:43.000000000",
+ "tz": 120
+ },
+ "subject": "Implement Feature X",
+ "message": "Implement Feature X\n\nAdded feature X."
+ }
+ }
+ }
+----
+
+If the change cannot be rebased, e.g. due to conflicts, the response is
+"`409 Conflict`" and the error message is contained in the response
+body.
+
+.Response
+----
+ HTTP/1.1 409 Conflict
+ Content-Disposition: attachment
+ Content-Type: text/plain;charset=UTF-8
+
+ The change could not be rebased due to a path conflict during merge.
+----
+
[[revert-change]]
Revert Change
~~~~~~~~~~~~~
@@ -746,7 +847,6 @@
"status": "NEW",
"created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000",
- "reviewed": true,
"mergeable": true,
"_sortkey": "0023412400000f7d",
"_number": 3965,
@@ -811,7 +911,6 @@
"status": "MERGED",
"created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000",
- "reviewed": true,
"_sortkey": "0023412400000f7d",
"_number": 3965,
"owner": {
@@ -833,6 +932,46 @@
blocked by Verified
----
+[[publish-draft-change]]
+Publish Draft Change
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/publish'
+
+Publishes a draft change.
+
+.Request
+----
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/publish HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
+[[delete-draft-change]]
+Delete Draft Change
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /changes/link:#change-id[\{change-id\}]'
+or
+'POST /changes/link:#change-id[\{change-id\}]/delete'
+
+Deletes a draft change.
+
+.Request
+----
+ DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940 HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
[[reviewer-endpoints]]
Reviewer Endpoints
------------------
@@ -1031,6 +1170,55 @@
Revision Endpoints
------------------
+[[get-commit]]
+Get Commit
+~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/commit'
+
+Retrieves a parsed commit of a revision.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/commit HTTP/1.0
+----
+
+As response a link:#commit-info[CommitInfo] entity is returned that
+describes the revision.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#commit",
+ "parents": [
+ {
+ "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
+ "subject": "Migrate contributor agreements to All-Projects."
+ }
+ ],
+ "author": {
+ "name": "Shawn O. Pearce",
+ "email": "sop@google.com",
+ "date": "2012-04-24 18:08:08.000000000",
+ "tz": -420
+ },
+ "committer": {
+ "name": "Shawn O. Pearce",
+ "email": "sop@google.com",
+ "date": "2012-04-24 18:08:08.000000000",
+ "tz": -420
+ },
+ "subject": "Use an EventBus to manage star icons",
+ "message": "Use an EventBus to manage star icons\n\nImage widgets that need to ..."
+ }
+----
+
+
[[get-review]]
Get Review
~~~~~~~~~~
@@ -1070,7 +1258,6 @@
"status": "NEW",
"created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000",
- "reviewed": true,
"mergeable": true,
"_sortkey": "0023412400000f7d",
"_number": 3965,
@@ -1218,6 +1405,95 @@
}
----
+[[rebase-revision]]
+Rebase Revision
+~~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/rebase'
+
+Rebases a revision.
+
+.Request
+----
+ POST /changes/myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/rebase HTTP/1.0
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the rebased change. Information about the current patch set
+is included.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#change",
+ "id": "myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
+ "project": "myProject",
+ "branch": "master",
+ "change_id": "I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
+ "subject": "Implement Feature X",
+ "status": "NEW",
+ "created": "2013-02-01 09:59:32.126000000",
+ "updated": "2013-02-21 11:16:36.775000000",
+ "mergeable": false,
+ "_sortkey": "0024cf9a000012bf",
+ "_number": 4799,
+ "owner": {
+ "name": "John Doe"
+ },
+ "current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
+ "revisions": {
+ "27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
+ "_number": 2,
+ "fetch": {
+ "http": {
+ "url": "http://gerrit:8080/myProject",
+ "ref": "refs/changes/99/4799/2"
+ }
+ },
+ "commit": {
+ "parents": [
+ {
+ "commit": "b4003890dadd406d80222bf1ad8aca09a4876b70",
+ "subject": "Implement Feature A"
+ }
+ ],
+ "author": {
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "date": "2013-05-07 15:21:27.000000000",
+ "tz": 120
+ },
+ "committer": {
+ "name": "Gerrit Code Review",
+ "email": "gerrit-server@example.com",
+ "date": "2013-05-07 15:35:43.000000000",
+ "tz": 120
+ },
+ "subject": "Implement Feature X",
+ "message": "Implement Feature X\n\nAdded feature X."
+ }
+ }
+ }
+----
+
+If the revision cannot be rebased, e.g. due to conflicts, the response is
+"`409 Conflict`" and the error message is contained in the response
+body.
+
+.Response
+----
+ HTTP/1.1 409 Conflict
+ Content-Disposition: attachment
+ Content-Type: text/plain;charset=UTF-8
+
+ The change could not be rebased due to a path conflict during merge.
+----
+
[[submit-revision]]
Submit Revision
~~~~~~~~~~~~~~~
@@ -1268,6 +1544,108 @@
"revision 674ac754f91e64a0efb8087e59a176484bd534d1 is not current revision"
----
+[[publish-draft-revision]]
+Publish Draft Revision
+~~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/publish'
+
+Publishes a draft revision.
+
+.Request
+----
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/publish HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
+[[delete-draft-revision]]
+Delete Draft Revision
+~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]'
+or
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/delete'
+
+Deletes a draft revision.
+
+.Request
+----
+ DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1 HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
+[[get-patch]]
+Get Patch
+~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/patch'
+
+Gets the formatted patch for one revision.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/patch HTTP/1.0
+----
+
+The formatted patch is returned as text encoded inside base64:
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: text/plain;charset=ISO-8859-1
+ X-FYI-Content-Encoding: base64
+ X-FYI-Content-Type: application/mbox
+
+ RnJvbSA3ZGFkY2MxNTNmZGVhMTdhYTg0ZmYzMmE2ZTI0NWRiYjY...
+----
+
+Adding query parameter `zip` (for example `/changes/.../patch?zip`)
+returns the patch as a single file inside of a ZIP archive. Clients
+can expand the ZIP to obtain the plain text patch, avoiding the
+need for a base64 decoding step. This option implies `download`.
+
+Query parameter `download` (e.g. `/changes/.../patch?download`)
+will suggest the browser save the patch as `commitsha1.diff.base64`,
+for later processing by command line tools.
+
+[[get-mergeable]]
+Get Mergeable
+~~~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/mergeable'
+
+Gets the method the server will use to submit (merge) the change and
+an indicator if the change is currently mergeable.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/mergeable HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ submit_type: "MERGE_IF_NECESSARY",
+ mergeable: true,
+ }
+----
+
[[get-submit-type]]
Get Submit Type
~~~~~~~~~~~~~~~
@@ -1319,7 +1697,7 @@
Content-Type: application/json;charset=UTF-8
)]}'
- "cherry_pick"
+ "CHERRY_PICK"
----
[[test-submit-rule]]
@@ -1641,13 +2019,261 @@
}
----
+[[list-files]]
+List Files
+~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/'
+
+Lists the files that were modified, added or deleted in a revision.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/ HTTP/1.0
+----
+
+As result a map is returned that maps the file path to a list of
+link:#file-info[FileInfo] entries. The entries in the map are
+sorted by file path.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "/COMMIT_MSG": {
+ "status": "A",
+ "lines_inserted": 7
+ },
+ "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": {
+ "lines_inserted": 5,
+ "lines_deleted": 3
+ }
+ }
+----
+
+The request parameter `reviewed` changes the response to return a list
+of the paths the caller has marked as reviewed. Clients that also
+need the FileInfo should make two requests.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/?reviewed HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ [
+ "/COMMIT_MSG",
+ "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+ ]
+----
+
+[[get-content]]
+Get Content
+~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/content'
+
+Gets the content of a file from a certain revision.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/content HTTP/1.0
+----
+
+The content is returned as base64 encoded string.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: text/plain;charset=UTF-8
+
+ Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
+----
+
+[[get-diff]]
+Get Diff
+~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/diff'
+
+Gets the diff of a file from a certain revision.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/diff HTTP/1.0
+----
+
+As response a link:#diff-info[DiffInfo] entity is returned that describes the diff.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]
+ {
+ "meta_a": {
+ "name": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+ "content_type": "text/x-java-source"
+ },
+ "meta_b": {
+ "name": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+ "content_type": "text/x-java-source"
+ },
+ "change_type": "MODIFIED",
+ "diff_header": [
+ "diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+ "index 59b7670..9faf81c 100644",
+ "--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+ "+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java"
+ ],
+ "content": [
+ {
+ "ab": [
+ "// Copyright (C) 2010 The Android Open Source Project",
+ "//",
+ "// Licensed under the Apache License, Version 2.0 (the \"License\");",
+ "// you may not use this file except in compliance with the License.",
+ "// You may obtain a copy of the License at",
+ "//",
+ "// http://www.apache.org/licenses/LICENSE-2.0",
+ "//",
+ "// Unless required by applicable law or agreed to in writing, software",
+ "// distributed under the License is distributed on an \"AS IS\" BASIS,",
+ "// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.",
+ "// See the License for the specific language governing permissions and",
+ "// limitations under the License."
+ ]
+ },
+ {
+ "b": [
+ "//",
+ "// Add some more lines in the header."
+ ]
+ },
+ {
+ "ab": [
+ "",
+ "package com.google.gerrit.server.project;",
+ "",
+ "import com.google.common.collect.Maps;",
+ ...
+ ]
+ }
+ ...
+ ]
+ }
+----
+
+If the `intraline` parameter is specified, intraline differences are included in the diff.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/b6b9c10649b9041884046119ab794374470a1b45/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/diff?intraline HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]
+ {
+ "meta_a": {
+ "name": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+ "content_type": "text/x-java-source"
+ },
+ "meta_b": {
+ "name": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+ "content_type": "text/x-java-source"
+ },
+ "change_type": "MODIFIED",
+ "diff_header": [
+ "diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+ "index 59b7670..9faf81c 100644",
+ "--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+ "+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java"
+ ],
+ "content": [
+ ...
+ {
+ "a": [
+ "/** Manages access control for Git references (aka branches, tags). */"
+ ],
+ "b": [
+ "/** Manages access control for the Git references (aka branches, tags). */"
+ ],
+ "edit_a": [],
+ "edit_b": [
+ [
+ 31,
+ 4
+ ]
+ ]
+ }
+ ]
+ }
+----
+
+The `base` parameter can be specified to control the base patch set from which the diff should
+be generated.
+
+.Request
+----
+ GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/b6b9c10649b9041884046119ab794374470a1b45/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/diff?base=2 HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]
+ {
+ "meta_a": {
+ "name": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+ "content_type": "text/x-java-source"
+ },
+ "meta_b": {
+ "name": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+ "content_type": "text/x-java-source"
+ },
+ "change_type": "MODIFIED",
+ "content": [
+ {
+ "skip": 578
+ }
+ ]
+ }
+----
+
+The `ignore-whitespace` parameter can be specified to control how whitespace differences are
+reported in the result. Valid values are `NONE`, `TRAILING`, `CHANGED` or `ALL`.
+
+The `context` parameter can be specified to control the number of lines of surrounding context
+in the diff. Valid values are `ALL` or number of lines.
+
[[set-reviewed]]
Set Reviewed
~~~~~~~~~~~~
[verse]
-'PUT /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#patch-id[\{patch-id\}]/reviewed'
+'PUT /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/reviewed'
-Marks a patch of a revision as reviewed by the calling user.
+Marks a file of a revision as reviewed by the calling user.
.Request
----
@@ -1659,16 +2285,16 @@
HTTP/1.1 201 Created
----
-If the patch was already marked as reviewed by the calling user the
+If the file was already marked as reviewed by the calling user the
response is "`200 OK`".
[[delete-reviewed]]
Delete Reviewed
~~~~~~~~~~~~~~~
[verse]
-'DELETE /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#patch-id[\{patch-id\}]/reviewed'
+'DELETE /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#file-id[\{file-id\}]/reviewed'
-Deletes the reviewed flag of the calling user from a patch of a revision.
+Deletes the reviewed flag of the calling user from a file of a revision.
.Request
----
@@ -1680,6 +2306,106 @@
HTTP/1.1 204 No Content
----
+[[cherry-pick]]
+Cherry Pick Revision
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/cherrypick'
+
+Cherry picks a revision to a destination branch.
+
+The commit message and destination branch must be provided in the request body inside a
+link:#cherrypick-input[CherryPickInput] entity.
+
+.Request
+----
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/cherrypick HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "message" : "Implementing Feature X",
+ "destination" : "release-branch"
+ }
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the resulting cherry picked change.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#change",
+ "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
+ "project": "myProject",
+ "branch": "release-branch",
+ "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941",
+ "subject": "Implementing Feature X",
+ "status": "NEW",
+ "created": "2013-02-01 09:59:32.126000000",
+ "updated": "2013-02-21 11:16:36.775000000",
+ "mergeable": true,
+ "_sortkey": "0023412400000f7d",
+ "_number": 3965,
+ "owner": {
+ "name": "John Doe"
+ }
+ }
+----
+
+[[message]]
+Edit Commit Message
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/message'
+
+Edit commit message.
+
+The commit message must be provided in the request body inside a
+link:#cherrypick-input[CherryPickInput] entity.
+
+.Request
+----
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/message HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "message" : "Reword Implementing Feature X",
+ }
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the change.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#change",
+ "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
+ "project": "myProject",
+ "branch": "release-branch",
+ "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941",
+ "subject": "Reword Implementing Feature X",
+ "status": "NEW",
+ "created": "2013-02-01 09:59:32.126000000",
+ "updated": "2013-02-21 11:16:36.775000000",
+ "mergeable": true,
+ "_sortkey": "0023412400000f7d",
+ "_number": 3965,
+ "owner": {
+ "name": "John Doe"
+ }
+ }
+----
[[ids]]
IDs
@@ -1715,10 +2441,10 @@
~~~~~~~~~~~~
UUID of a draft comment.
-[[patch-id]]
-\{patch-id\}
+[[file-id]]
+\{file-id\}
~~~~~~~~~~~~
-The file path of the patch.
+The path of the file.
[[revision-id]]
\{revision-id\}
@@ -1733,7 +2459,6 @@
change ("674ac754"), at least 4 digits are required
* a legacy numeric patch number ("1" for first patch set of the change)
-
[[json-entities]]
JSON Entities
-------------
@@ -1751,6 +2476,33 @@
change.
|===========================
+[[action-info]]
+ActionInfo
+~~~~~~~~~~
+The `ActionInfo` entity describes a REST API call the client can
+make to manipulate a resource. These are frequently implemented by
+plugins and may be discovered at runtime.
+
+[options="header",width="50%",cols="1,^1,5"]
+|====================================
+|Field Name ||Description
+|`method` |optional|
+HTTP method to use with the action. Most actions use `POST`, `PUT`
+or `DELETE` to cause state changes.
+|`label` |optional|
+Short title to display to a user describing the action. In the
+Gerrit web interface the label is used as the text on the button
+presented in the UI.
+|`title` |optional|
+Longer text to display describing the action. In a web UI this
+should be the title attribute of the element, displaying when
+the user hovers the mouse.
+|`enabled` |optional|
+If true the action is permitted at this time and the caller is
+likely allowed to execute it. This may change if state is updated
+at the server or permissions are modified. Not present if false.
+|====================================
+
[[add-reviewer-result]]
AddReviewerResult
~~~~~~~~~~~~~~~~~
@@ -1788,6 +2540,8 @@
The vote that the user has given for the label. If present and zero, the
user is permitted to vote on the label. If absent, the user is not
permitted to vote on that label.
+|`date` |optional|
+The time and date describing when the approval was made.
|===========================
[[change-info]]
@@ -1824,6 +2578,7 @@
Whether the calling user has starred this change.
|`reviewed` |not set if `false`|
Whether the change was reviewed by the calling user.
+Only set if link:#reviewed[reviewed] is requested.
|`mergeable` |optional|
Whether the change is mergeable. +
Not set for merged changes.
@@ -1832,6 +2587,10 @@
|`owner` ||
The owner of the change as an link:rest-api-accounts.html#account-info[
AccountInfo] entity.
+|`actions` |optional|
+Actions the caller might be able to perform on this revision. The
+information is a map of view name to link:#action-info[ActionInfo]
+entities.
|`labels` |optional|
The labels of the change as a map that maps the label names to
link:#label-info[LabelInfo] entries. +
@@ -1846,7 +2605,7 @@
link:rest-api-accounts.html#account-info[AccountInfo] entities. +
Only set if link:#detailed-labels[detailed labels] are requested.
|`messages`|optional|
-Messages associated with the change as a list of
+Messages associated with the change as a list of
link:#change-message-info[ChangeMessageInfo] entities. +
Only set if link:#messages[messages] are requested.
|`current_revision` |optional|
@@ -1883,6 +2642,18 @@
Which patchset (if any) generated this message.
|==================================
+[[cherrypick-input]]
+CherryPickInput
+~~~~~~~~~~~~~~~
+The `CherryPickInput` entity contains information for cherry-picking a change to a new branch.
+
+[options="header",width="50%",cols="1,6"]
+|===========================
+|Field Name |Description
+|`message` |Commit message for the cherry-picked change
+|`destination` |Destination Branch
+|===========================
+
[[comment-info]]
CommentInfo
~~~~~~~~~~~
@@ -1902,7 +2673,11 @@
If not set, the default is `REVISION`.
|`line` |optional|
The number of the line for which the comment was done. +
-If not set, it's a file comment.
+If range is set, this equals the end line of the range. +
+If neither line nor range is set, it's a file comment.
+|`range` |optional|
+The range of the comment as a link:rest-api.html#comment-range[CommentRange]
+entity.
|`in_reply_to` |optional|
The URL encoded UUID of the comment to which this comment is a reply.
|`message` |optional|The comment message.
@@ -1910,7 +2685,7 @@
The link:rest-api.html#timestamp[timestamp] of when this comment was
written.
|`author` |optional|
-The author of the message as an +
+The author of the message as an
link:rest-api-accounts.html#account-info[AccountInfo] entity. +
Unset for draft comments, assumed to be the calling user.
|===========================
@@ -1940,7 +2715,11 @@
|`line` |optional|
The number of the line for which the comment should be added. +
`0` if it is a file comment. +
-If not set, a file comment is added.
+If neither line nor range is set, a file comment is added. +
+If range is set, this should equal the end line of the range.
+|`range` |optional|
+The range of the comment as a link:rest-api.html#comment-range[CommentRange]
+entity.
|`in_reply_to` |optional|
The URL encoded UUID of the comment to which this comment is a reply.
|`updated` |optional|
@@ -1952,6 +2731,20 @@
comment is deleted.
|===========================
+[[comment-range]]
+CommentRange
+~~~~~~~~~~~~
+The `CommentRange` entity describes the range of an inline comment.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name ||Description
+|`start_line` ||The start line number of the range.
+|`start_character` ||The character position in the start line.
+|`end_line` ||The end line number of the range.
+|`end_character` ||The character position in the end line.
+|===========================
+
[[commit-info]]
CommitInfo
~~~~~~~~~~
@@ -1963,7 +2756,8 @@
|`commit` |The commit ID.
|`parent` |
The parent commits of this commit as a list of
-link:#commit-info[CommitInfo] entities.
+link:#commit-info[CommitInfo] entities. In parent
+only `commit` and `subject` fields are populated.
|`author` |The author of the commit as a
link:#git-person-info[GitPersonInfo] entity.
|`committer` |The committer of the commit as a
@@ -1973,6 +2767,79 @@
|`message` |The commit message.
|==========================
+[[diff-content]]
+DiffContent
+~~~~~~~~~~~
+The `DiffContent` entity contains information about the content differences
+in a file.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==========================
+|Field Name ||Description
+|`a` |optional|Content only in the file on side A (deleted in B).
+|`b` |optional|Content only in the file on side B (added in B).
+|`ab` |optional|Content in the file on both sides (unchanged).
+|`edit_a` |only present during a replace, i.e. both `a` and `b` are present|
+Text sections deleted from side A as a
+link:#diff-intraline-info[DiffIntralineInfo] entity.
+|`edit_b` |only present during a replace, i.e. both `a` and `b` are present|
+Text sections inserted in side B as a
+link:#diff-intraline-info[DiffIntralineInfo] entity.
+|`skip` |optional|count of lines skipped on both sides when the file is
+too large to include all common lines.
+|==========================
+
+[[diff-file-meta-info]]
+DiffFileMetaInfo
+~~~~~~~~~~~~~~~~
+The `DiffFileMetaInfo` entity contains meta information about a file diff.
+
+[options="header",width="50%",cols="1,6"]
+|==========================
+|Field Name |Description
+|`name` |The name of the file.
+|`content_type`|The content type of the file.
+|==========================
+
+[[diff-info]]
+DiffInfo
+~~~~~~~~
+The `DiffInfo` entity contains information about the diff of a file
+in a revision.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==========================
+|Field Name ||Description
+|`meta_a` |not present when the file is added|
+Meta information about the file on side A as a
+link:#diff-file-meta-info[DiffFileMetaInfo] entity.
+|`meta_b` |not present when the file is deleted|
+Meta information about the file on side B as a
+link:#diff-file-meta-info[DiffFileMetaInfo] entity.
+|`change_type` ||The type of change (`ADDED`, `MODIFIED`, `DELETED`, `RENAMED`
+`COPIED`, `REWRITE`).
+|`intraline_status`|only set when the `intraline` parameter was specified in the request|
+Intraline status (`OK`, `ERROR`, `TIMEOUT`).
+|`diff_header` ||A list of strings representing the patch set diff header.
+|`content` ||The content differences in the file as a list of
+link:#diff-content[DiffContent] entities.
+|==========================
+
+[[diff-intraline-info]]
+DiffIntralineInfo
+~~~~~~~~~~~~~~~~~
+The `DiffIntralineInfo` entity contains information about intraline edits in a
+file.
+
+The information consists of a list of `<skip length, mark length>` pairs, where
+the skip length is the number of characters between the end of the previous edit
+and the start of this edit, and the mark length is the number of edited characters
+following the skip. The start of the edits is from the beginning of the related
+diff content lines.
+
+Note that the implied newline character at the end of each line is included in
+the length calculation, and thus it is possible for the edits to span newlines.
+
[[fetch-info]]
FetchInfo
~~~~~~~~~
@@ -2030,40 +2897,62 @@
[[label-info]]
LabelInfo
~~~~~~~~~
-The `LabelInfo` entity contains information about a label on a change.
+The `LabelInfo` entity contains information about a label on a change, always
+corresponding to the current patch set.
+There are two options that control the contents of `LabelInfo`:
+link:#labels[`LABELS`] and link:#detailed-labels[`DETAILED_LABELS`].
+
+* For a quick summary of the state of labels, use `LABELS`.
+* For detailed information about labels, including exact numeric votes for all
+ users and the allowed range of votes for the current user, use `DETAILED_LABELS`.
+
+Common fields
+^^^^^^^^^^^^^
[options="header",width="50%",cols="1,^1,5"]
|===========================
|Field Name ||Description
-|`approved` |optional|The user who approved this label on the change
-as a link:rest-api-accounts.html#account-info[AccountInfo] entity. +
-Only set if link:#labels[labels] are requested.
-|`rejected` |optional|The user who rejected this label on the change
-as a link:rest-api-accounts.html#account-info[AccountInfo] entity. +
-Only set if link:#labels[labels] are requested.
-|`recommended` |optional|The user who recommended this label on the
-change as a link:rest-api-accounts.html#account-info[AccountInfo] entity. +
-Only set if link:#labels[labels] are requested.
-|`disliked` |optional|The user who disliked this label on the change
-as a link:rest-api-accounts.html#account-info[AccountInfo] entity. +
-Only set if link:#labels[labels] are requested.
-|`value` |optional|The voting value of the user who
-recommended/disliked this label on the change if it is not
-"`+1`"/"`-1`". +
-Only set if link:#labels[labels] are requested.
|`optional` |not set if `false`|
Whether the label is optional. Optional means the label may be set, but
it's neither necessary for submission nor does it block submission if
set.
+|===========================
+
+Fields set by `LABELS`
+^^^^^^^^^^^^^^^^^^^^^^
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name ||Description
+|`approved` |optional|One user who approved this label on the change
+(voted the maximum value) as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`rejected` |optional|One user who rejected this label on the change
+(voted the minimum value) as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`recommended` |optional|One user who recommended this label on the
+change (voted positively, but not the maximum value) as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`disliked` |optional|One user who disliked this label on the change
+(voted negatively, but not the minimum value) as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity.
+|`value` |optional|The voting value of the user who
+recommended/disliked this label on the change if it is not
+"`+1`"/"`-1`".
+|===========================
+
+Fields set by `DETAILED_LABELS`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name ||Description
|`all` |optional|List of all approvals for this label as a list
-of link:#approval-info[ApprovalInfo] entities. +
-Only set if link:#detailed-labels[detailed labels] are requested.
+of link:#approval-info[ApprovalInfo] entities.
|`values` |optional|A map of all values that are allowed for this
label. The map maps the values ("`-2`", "`-1`", " `0`", "`+1`", "`+2`")
-to the value descriptions. +
-Only set if link:#detailed-labels[detailed labels] are requested.
+to the value descriptions.
|===========================
+
[[restore-input]]
RestoreInput
~~~~~~~~~~~~
@@ -2138,6 +3027,14 @@
after the review is stored. +
Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
If not set, the default is `ALL`.
+|`on_behalf_of`|optional|
+link:rest-api-accounts.html#account-id[\{account-id\}] the review
+should be posted on behalf of. To use this option the caller must
+have been granted `labelAs-NAME` permission for all keys of labels.
+|`wait_for_commit`|optional|
+Whether the request should wait for commit to the index to finish.
+If `false` (default) the request returns after the data is sent to
+the index, but searches may not immediately see the update.
|============================
[[reviewer-info]]
@@ -2196,11 +3093,15 @@
Information about how to fetch this patch set. The fetch information is
provided as a map that maps the protocol name ("`git`", "`http`",
"`ssh`") to link:#fetch-info[FetchInfo] entities.
-|`commit` ||The commit of the patch set as
+|`commit` |optional|The commit of the patch set as
link:#commit-info[CommitInfo] entity.
-|`files` ||
+|`files` |optional|
The files of the patch set as a map that maps the file names to
link:#file-info[FileInfo] entities.
+|`actions` |optional|
+Actions the caller might be able to perform on this revision. The
+information is a map of view name to link:#action-info[ActionInfo]
+entities.
|===========================
[[rule-input]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
new file mode 100644
index 0000000..f566704
--- /dev/null
+++ b/Documentation/rest-api-config.txt
@@ -0,0 +1,154 @@
+Gerrit Code Review - /config/ REST API
+======================================
+
+This page describes the config related REST endpoints.
+Please also take note of the general information on the
+link:rest-api.html[REST API].
+
+[[config-endpoints]]
+Config Endpoints
+---------------
+
+[[get-version]]
+Get Version
+~~~~~~~~~~~
+[verse]
+'GET /config/server/version'
+
+Returns the version of the Gerrit server.
+
+.Request
+----
+ GET /config/server/version HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ "2.7"
+----
+
+[[list-capabilities]]
+List Capabilities
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /config/server/capabilities'
+
+Lists the capabilities that are available in the system. There are two
+kinds of capabilities: core and plugin-owned capabilities.
+
+As result a map of link:#capability-info[CapabilityInfo] entities is
+returned.
+
+The entries in the map are sorted by capability ID.
+
+.Request
+----
+ GET /config/server/capabilities/ HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "accessDatabase": {
+ "kind": "gerritcodereview#capability",
+ "id": "accessDatabase",
+ "name": "Access Database"
+ },
+ "administrateServer": {
+ "kind": "gerritcodereview#capability",
+ "id": "administrateServer",
+ "name": "Administrate Server"
+ },
+ "createAccount": {
+ "kind": "gerritcodereview#capability",
+ "id": "createAccount",
+ "name": "Create Account"
+ },
+ "createGroup": {
+ "kind": "gerritcodereview#capability",
+ "id": "createGroup",
+ "name": "Create Group"
+ },
+ "createProject": {
+ "kind": "gerritcodereview#capability",
+ "id": "createProject",
+ "name": "Create Project"
+ },
+ "emailReviewers": {
+ "kind": "gerritcodereview#capability",
+ "id": "emailReviewers",
+ "name": "Email Reviewers"
+ },
+ "flushCaches": {
+ "kind": "gerritcodereview#capability",
+ "id": "flushCaches",
+ "name": "Flush Caches"
+ },
+ "killTask": {
+ "kind": "gerritcodereview#capability",
+ "id": "killTask",
+ "name": "Kill Task"
+ },
+ "priority": {
+ "kind": "gerritcodereview#capability",
+ "id": "priority",
+ "name": "Priority"
+ },
+ "queryLimit": {
+ "kind": "gerritcodereview#capability",
+ "id": "queryLimit",
+ "name": "Query Limit"
+ },
+ "runGC": {
+ "kind": "gerritcodereview#capability",
+ "id": "runGC",
+ "name": "Run Garbage Collection"
+ },
+ "streamEvents": {
+ "kind": "gerritcodereview#capability",
+ "id": "streamEvents",
+ "name": "Stream Events"
+ },
+ "viewCaches": {
+ "kind": "gerritcodereview#capability",
+ "id": "viewCaches",
+ "name": "View Caches"
+ },
+ "viewConnections": {
+ "kind": "gerritcodereview#capability",
+ "id": "viewConnections",
+ "name": "View Connections"
+ },
+ "viewQueue": {
+ "kind": "gerritcodereview#capability",
+ "id": "viewQueue",
+ "name": "View Queue"
+ }
+ }
+----
+
+[[capability-info]]
+CapabilityInfo
+~~~~~~~~~~~~~~
+The `CapabilityInfo` entity contains information about a capability.
+
+[options="header",width="50%",cols="1,5"]
+|=================================
+|Field Name |Description
+|`kind` |`gerritcodereview#capability`
+|`id` |capability ID
+|`name` |capability name
+|=================================
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index e800d56..6289e5a 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -293,12 +293,14 @@
{
"_account_id": 1000097,
"name": "Jane Roe",
- "email": "jane.roe@example.com"
+ "email": "jane.roe@example.com",
+ "username": "jane"
},
{
"_account_id": 1000096,
"name": "John Doe",
"email": "john.doe@example.com"
+ "username": "john"
}
],
"includes": []
@@ -620,12 +622,14 @@
{
"_account_id": 1000097,
"name": "Jane Roe",
- "email": "jane.roe@example.com"
+ "email": "jane.roe@example.com",
+ "username": "jane"
},
{
"_account_id": 1000096,
"name": "John Doe",
- "email": "john.doe@example.com"
+ "email": "john.doe@example.com",
+ "username": "john"
}
]
----
@@ -657,17 +661,20 @@
{
"_account_id": 1000097,
"name": "Jane Roe",
- "email": "jane.roe@example.com"
+ "email": "jane.roe@example.com",
+ "username": "jane"
},
{
"_account_id": 1000096,
"name": "John Doe",
- "email": "john.doe@example.com"
+ "email": "john.doe@example.com",
+ "username": "john"
},
{
"_account_id": 1000098,
"name": "Richard Roe",
- "email": "richard.roe@example.com"
+ "email": "richard.roe@example.com",
+ "username": "rroe"
}
]
----
@@ -698,7 +705,8 @@
{
"_account_id": 1000096,
"name": "John Doe",
- "email": "john.doe@example.com"
+ "email": "john.doe@example.com",
+ "username": "john"
}
----
@@ -728,7 +736,8 @@
{
"_account_id": 1000037,
"name": "John Doe",
- "email": "john.doe@example.com"
+ "email": "john.doe@example.com",
+ "username": "john"
}
----
@@ -756,10 +765,10 @@
Content-Type: application/json;charset=UTF-8
{
- "members": {
+ "members": [
"jane.roe@example.com",
"john.doe@example.com"
- }
+ ]
}
----
@@ -782,12 +791,14 @@
{
"_account_id": 1000057,
"name": "Jane Roe",
- "email": "jane.roe@example.com"
+ "email": "jane.roe@example.com",
+ "username": "jane"
},
{
"_account_id": 1000037,
"name": "John Doe",
- "email": "john.doe@example.com"
+ "email": "john.doe@example.com",
+ "username": "john"
}
]
----
@@ -827,10 +838,10 @@
Content-Type: application/json;charset=UTF-8
{
- "members": {
+ "members": [
"jane.roe@example.com",
"john.doe@example.com"
- }
+ ]
}
----
@@ -923,7 +934,8 @@
[verse]
'PUT /groups/link:#group-id[\{group-id\}]/groups/link:#group-id[\{group-id\}]'
-Includes a group into a Gerrit internal group.
+Includes an internal or external group into a Gerrit internal group.
+External groups must be specified using the UUID.
.Request
----
diff --git a/Documentation/rest-api-plugins.txt b/Documentation/rest-api-plugins.txt
new file mode 100644
index 0000000..73095cc
--- /dev/null
+++ b/Documentation/rest-api-plugins.txt
@@ -0,0 +1,278 @@
+Gerrit Code Review - /plugins/ REST API
+=======================================
+
+This page describes the plugin related REST endpoints.
+Please also take note of the general information on the
+link:rest-api.html[REST API].
+
+[[plugin-endpoints]]
+Plugin Endpoints
+----------------
+
+Gerrit REST endpoints for installed plugins are available under
+'/plugins/link:#plugin-id[\{plugin-id\}]/gerrit~<endpoint-id>'.
+The `gerrit~` prefix ensures that the Gerrit REST endpoints for plugins
+do not clash with any REST endpoint that a plugin may offer under its
+namespace.
+
+
+[[list-plugins]]
+List Plugins
+~~~~~~~~~~~~
+[verse]
+'GET /plugins/'
+
+Lists the plugins installed on the Gerrit server. Only the enabled
+plugins are returned unless the `all` option is specified.
+
+As result a map is returned that maps the plugin IDs to
+link:#plugin-info[PluginInfo] entries. The entries in the map are sorted
+by plugin ID.
+
+.Request
+----
+ GET /plugins/?all HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "delete-project": {
+ "kind": "gerritcodereview#plugin",
+ "id": "delete-project",
+ "version": "2.8-SNAPSHOT"
+ },
+ "reviewers-by-blame": {
+ "kind": "gerritcodereview#plugin",
+ "id": "reviewers-by-blame",
+ "version": "2.8-SNAPSHOT",
+ "disabled": true
+ }
+ }
+----
+
+[[install-plugin]]
+Install Plugin
+~~~~~~~~~~~~~~
+[verse]
+'PUT /plugins/link:#plugin-id[\{plugin-id\}]'
+
+Installs a new plugin on the Gerrit server. If a plugin with the
+specified name already exists it is overwritten.
+
+The plugin jar can either be sent as binary data in the request body
+or a URL to the plugin jar must be provided in the request body inside
+a link:#plugin-input[PluginInput] entity.
+
+.Request
+----
+ PUT /plugins/delete-project HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "url": "file:///gerrit/plugins/delete-project/delete-project-2.8.jar"
+ }
+----
+
+To provide the plugin jar as binary data in the request body the
+following curl command can be used:
+
+----
+ curl --digest --user admin:TNNuLkWsIV8w -X PUT --data-binary @delete-project-2.8.jar 'http://gerrit:8080/a/plugins/delete-project'
+----
+
+As response a link:#plugin-info[PluginInfo] entity is returned that
+describes the plugin.
+
+.Response
+----
+ HTTP/1.1 201 Created
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#plugin",
+ "id": "delete-project",
+ "version": "2.8"
+ }
+----
+
+If an existing plugin was overwritten the response is "`200 OK`".
+
+[[get-plugin-status]]
+Get Plugin Status
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /plugins/link:#plugin-id[\{plugin-id\}]/gerrit~status'
+
+Retrieves the status of a plugin on the Gerrit server.
+
+.Request
+----
+ GET /plugins/delete-project/gerrit~status HTTP/1.0
+----
+
+As response a link:#plugin-info[PluginInfo] entity is returned that
+describes the plugin.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#plugin",
+ "id": "delete-project",
+ "version": "2.8"
+ }
+----
+
+[[enable-plugin]]
+Enable Plugin
+~~~~~~~~~~~~~
+[verse]
+'POST /plugins/link:#plugin-id[\{plugin-id\}]/gerrit~enable'
+
+Enables a plugin on the Gerrit server.
+
+.Request
+----
+ POST /plugins/delete-project/gerrit~enable HTTP/1.0
+----
+
+As response a link:#plugin-info[PluginInfo] entity is returned that
+describes the plugin.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#plugin",
+ "id": "delete-project",
+ "version": "2.8"
+ }
+----
+
+[[disable-plugin]]
+Disable Plugin
+~~~~~~~~~~~~~~
+[verse]
+'POST /plugins/link:#plugin-id[\{plugin-id\}]/gerrit~disable'
+
+OR
+
+[verse]
+'DELETE /plugins/link:#plugin-id[\{plugin-id\}]'
+
+Disables a plugin on the Gerrit server.
+
+.Request
+----
+ POST /plugins/delete-project/gerrit~disable HTTP/1.0
+----
+
+As response a link:#plugin-info[PluginInfo] entity is returned that
+describes the plugin.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#plugin",
+ "id": "delete-project",
+ "version": "2.8",
+ "disabled": true
+ }
+----
+
+[[reload-plugin]]
+Reload Plugin
+~~~~~~~~~~~~~
+[verse]
+'POST /plugins/link:#plugin-id[\{plugin-id\}]/gerrit~reload'
+
+Reloads a plugin on the Gerrit server.
+
+.Request
+----
+ POST /plugins/delete-project/gerrit~reload HTTP/1.0
+----
+
+As response a link:#plugin-info[PluginInfo] entity is returned that
+describes the plugin.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#plugin",
+ "id": "delete-project",
+ "version": "2.8",
+ "disabled": true
+ }
+----
+
+
+[[ids]]
+IDs
+---
+
+[[plugin-id]]
+\{plugin-id\}
+~~~~~~~~~~~~~
+The ID of the plugin.
+
+
+[[json-entities]]
+JSON Entities
+-------------
+
+[[plugin-info]]
+PluginInfo
+~~~~~~~~~~
+The `PluginInfo` entity describes a plugin.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=======================
+|Field Name||Description
+|`kind` ||`gerritcodereview#plugin`
+|`id` ||The ID of the plugin.
+|`version` ||The version of the plugin.
+|`disabled`|not set if `false`|Whether the plugin is disabled.
+|=======================
+
+[[plugin-input]]
+PluginInput
+~~~~~~~~~~~
+The `PluginInput` entity describes a plugin that should be installed.
+
+[options="header",width="50%",cols="1,6"]
+|======================
+|Field Name|Description
+|`url` |URL to the plugin jar.
+|======================
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 35caac4..a347d81 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -5,9 +5,6 @@
Please also take note of the general information on the
link:rest-api.html[REST API].
-Endpoints
----------
-
[[project-endpoints]]
Project Endpoints
-----------------
@@ -436,6 +433,7 @@
)]}'
{
"kind": "gerritcodereview#project_config",
+ "description": "demo project",
"use_contributor_agreements": {
"value": true,
"configured_value": "TRUE",
@@ -455,7 +453,85 @@
"value": false,
"configured_value": "FALSE",
"inherited_value": true
- }
+ },
+ "max_object_size_limit": {
+ "value": "15m",
+ "configured_value": "15m",
+ "inherited_value": "20m"
+ },
+ "submit_type": "MERGE_IF_NECESSARY",
+ "state": "ACTIVE",
+ "commentlinks": {}
+ }
+----
+
+[[set-config]]
+Set Config
+~~~~~~~~~~
+[verse]
+'PUT /projects/link:#project-name[\{project-name\}]/config'
+
+Sets the configuration of a project.
+
+The new configuration must be provided in the request body as a
+link:#config-input[ConfigInput] entity.
+
+.Request
+----
+ PUT /projects/myproject/config HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "description": "demo project",
+ "use_contributor_agreements": "FALSE",
+ "use_content_merge": "INHERIT",
+ "use_signed_off_by": "INHERIT",
+ "require_change_id": "TRUE",
+ "max_object_size_limit": "10m",
+ "submit_type": "REBASE_IF_NECESSARY",
+ "state": "ACTIVE"
+ }
+----
+
+As response the new configuration is returned as a link:#config-info[
+ConfigInfo] entity.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#project_config",
+ "use_contributor_agreements": {
+ "value": false,
+ "configured_value": "FALSE",
+ "inherited_value": false
+ },
+ "use_content_merge": {
+ "value": true,
+ "configured_value": "INHERIT",
+ "inherited_value": true
+ },
+ "use_signed_off_by": {
+ "value": false,
+ "configured_value": "INHERIT",
+ "inherited_value": false
+ },
+ "require_change_id": {
+ "value": true,
+ "configured_value": "TRUE",
+ "inherited_value": true
+ },
+ "max_object_size_limit": {
+ "value": "10m",
+ "configured_value": "10m",
+ "inherited_value": "20m"
+ },
+ "submit_type": "REBASE_IF_NECESSARY",
+ "state": "ACTIVE",
"commentlinks": {}
}
----
@@ -499,6 +575,281 @@
done.
----
+[[branch-endpoints]]
+Branch Endpoints
+----------------
+
+[[list-branches]]
+List Branches
+~~~~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/branches/'
+
+List the branches of a project.
+
+As result a list of link:#branch-info[BranchInfo] entries is
+returned.
+
+.Request
+----
+ GET /projects/work%2Fmy-project/branches/ HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ [
+ {
+ "ref": "HEAD",
+ "revision": "master"
+ },
+ {
+ "ref": "refs/meta/config",
+ "revision": "76016386a0d8ecc7b6be212424978bb45959d668"
+ },
+ {
+ "ref": "refs/heads/master",
+ "revision": "67ebf73496383c6777035e374d2d664009e2aa5c"
+ },
+ {
+ "ref": "refs/heads/stable",
+ "revision": "64ca533bd0eb5252d2fee83f63da67caae9b4674",
+ "can_delete": true
+ }
+ ]
+----
+
+[[get-branch]]
+Get Branch
+~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/branches/link:#branch-id[\{branch-id\}]'
+
+Retrieves a branch of a project.
+
+.Request
+----
+ GET /projects/work%2Fmy-project/branches/master HTTP/1.0
+----
+
+As response a link:#branch-info[BranchInfo] entity is returned that
+describes the branch.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "ref": "refs/heads/master",
+ "revision": "67ebf73496383c6777035e374d2d664009e2aa5c"
+ }
+----
+
+[[create-branch]]
+Create Branch
+~~~~~~~~~~~~~
+[verse]
+'PUT /projects/link:#project-name[\{project-name\}]/branches/link:#branch-id[\{branch-id\}]'
+
+Creates a new branch.
+
+In the request body additional data for the branch can be provided as
+link:#branch-input[BranchInput].
+
+.Request
+----
+ PUT /projects/MyProject/branches/stable HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "revision": "76016386a0d8ecc7b6be212424978bb45959d668"
+ }
+----
+
+As response a link:#branch-info[BranchInfo] entity is returned that
+describes the created branch.
+
+.Response
+----
+ HTTP/1.1 201 Created
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "ref": "refs/heads/stable",
+ "revision": "76016386a0d8ecc7b6be212424978bb45959d668",
+ "can_delete": true
+ }
+----
+
+[[delete-branch]]
+Delete Branch
+~~~~~~~~~~~~~
+[verse]
+'DELETE /projects/link:#project-name[\{project-name\}]/branches/link:#branch-id[\{branch-id\}]'
+
+Deletes a branch.
+
+.Request
+----
+ DELETE /projects/MyProject/branches/stable HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
+[[child-project-endpoints]]
+Child Project Endpoints
+-----------------------
+
+[[list-child-projects]]
+List Child Projects
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/children/'
+
+List the direct child projects of a project.
+
+.Request
+----
+ GET /projects/Public-Plugins/children/ HTTP/1.0
+----
+
+As result a list of link:#project-info[ProjectInfo] entries is
+returned that describe the child projects.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ [
+ {
+ "kind": "gerritcodereview#project",
+ "id": "plugins%2Freplication",
+ "name": "plugins/replication",
+ "parent": "Public-Plugins",
+ "description": "Copies to other servers using the Git protocol"
+ },
+ {
+ "kind": "gerritcodereview#project",
+ "id": "plugins%2Freviewnotes",
+ "name": "plugins/reviewnotes",
+ "parent": "Public-Plugins",
+ "description": "Annotates merged commits using notes on refs/notes/review."
+ },
+ {
+ "kind": "gerritcodereview#project",
+ "id": "plugins%2Fsingleusergroup",
+ "name": "plugins/singleusergroup",
+ "parent": "Public-Plugins",
+ "description": "GroupBackend enabling users to be directly added to access rules"
+ }
+ ]
+----
+
+To resolve the child projects of a project recursively the parameter
+`recursive` can be set.
+
+Child projects that are not visible to the calling user are ignored and
+are not resolved further.
+
+.Request
+----
+ GET /projects/Public-Projects/children/?recursive HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ [
+ {
+ "kind": "gerritcodereview#project",
+ "id": "gerrit",
+ "name": "gerrit",
+ "parent": "Public-Projects",
+ "description": "Gerrit Code Review"
+ },
+ {
+ "kind": "gerritcodereview#project",
+ "id": "plugins%2Freplication",
+ "name": "plugins/replication",
+ "parent": "Public-Plugins",
+ "description": "Copies to other servers using the Git protocol"
+ },
+ {
+ "kind": "gerritcodereview#project",
+ "id": "plugins%2Freviewnotes",
+ "name": "plugins/reviewnotes",
+ "parent": "Public-Plugins",
+ "description": "Annotates merged commits using notes on refs/notes/review."
+ },
+ {
+ "kind": "gerritcodereview#project",
+ "id": "plugins%2Fsingleusergroup",
+ "name": "plugins/singleusergroup",
+ "parent": "Public-Plugins",
+ "description": "GroupBackend enabling users to be directly added to access rules"
+ },
+ {
+ "kind": "gerritcodereview#project",
+ "id": "Public-Plugins",
+ "name": "Public-Plugins",
+ "parent": "Public-Projects",
+ "description": "Parent project for plugins/*"
+ }
+ ]
+----
+
+[[get-child-project]]
+Get Child Project
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/children/link:#project-name[\{project-name\}]'
+
+Retrieves a child project. If a non-direct child project should be
+retrieved the parameter `recursive` must be set.
+
+.Request
+----
+ GET /projects/Public-Plugins/children/plugins%2Freplication HTTP/1.0
+----
+
+As response a link:#project-info[ProjectInfo] entity is returned that
+describes the child project.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "kind": "gerritcodereview#project",
+ "id": "plugins%2Freplication",
+ "name": "plugins/replication",
+ "parent": "Public-Plugins",
+ "description": "Copies to other servers using the Git protocol"
+ }
+----
+
[[dashboard-endpoints]]
Dashboard Endpoints
-------------------
@@ -727,6 +1078,12 @@
IDs
---
+[[branch-id]]
+\{branch-id\}
+~~~~~~~~~~~~~
+The name of a branch or `HEAD`. The prefix `refs/heads/` can be
+omitted.
+
[[dashboard-id]]
\{dashboard-id\}
~~~~~~~~~~~~~~~~
@@ -745,42 +1102,140 @@
JSON Entities
-------------
+[[branch-info]]
+BranchInfo
+~~~~~~~~~~
+The `BranchInfo` entity contains information about a branch.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=========================
+|Field Name ||Description
+|`ref` ||The ref of the branch.
+|`revision` ||The revision to which the branch points.
+|`can_delete`|`false` if not set|
+Whether the calling user can delete this branch.
+|=========================
+
+[[branch-input]]
+BranchInput
+~~~~~~~~~~~
+The `BranchInput` entity contains information for the creation of
+a new branch.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=======================
+|Field Name||Description
+|`ref` |optional|
+The name of the branch. The prefix `refs/heads/` can be
+omitted. +
+If set, must match the branch ID in the URL.
+|`revision`|optional|
+The base revision of the new branch. +
+If not set, `HEAD` will be used as base revision.
+|=======================
+
[[config-info]]
ConfigInfo
~~~~~~~~~~
The `ConfigInfo` entity contains information about the effective project
configuration.
-Fields marked with * are only visible to users who have read access to
-`refs/meta/config`.
-
-[options="header",width="50%",cols="1,6"]
-|======================================
-|Field Name |Description
-|`use_contributor_agreements*`|
+[options="header",width="50%",cols="1,^2,4"]
+|=========================================
+|Field Name ||Description
+|`description` |optional|
+The description of the project.
+|`use_contributor_agreements`|optional|
link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
authors must complete a contributor agreement on the site before
pushing any commits or changes to this project.
-|`use_content_merge*`|
+|`use_content_merge` |optional|
link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
Gerrit will try to perform a 3-way merge of text file content when a
file has been modified by both the destination branch and the change
being submitted. This option only takes effect if submit type is not
FAST_FORWARD_ONLY.
-|`use_signed_off_by*`|
+|`use_signed_off_by` |optional|
link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
each change must contain a Signed-off-by line from either the author or
the uploader in the commit message.
-|`require_change_id*`|
+|`require_change_id` |optional|
link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether a
valid link:user-changeid.html[Change-Id] footer in any commit uploaded
for review is required. This does not apply to commits pushed directly
to a branch or tag.
-|`commentlinks`|
-Comment link configuration for the project. Has the same format as the
-link:config-gerrit.html#_a_id_commentlink_a_section_commentlink[commentlink section]
-of `gerrit.config`.
-|======================================
+|`max_object_size_limit` ||
+The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
+limit] of this project as a link:#max-object-size-limit-info[
+MaxObjectSizeLimitInfo] entity.
+|`submit_type` ||
+The default submit type of the project, can be `MERGE_IF_NECESSARY`,
+`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
+`CHERRY_PICK`.
+|`state` |optional|
+The state of the project, can be `ACTIVE`, `READ_ONLY` or `HIDDEN`. +
+Not set if the project state is `ACTIVE`.
+|`commentlinks` ||
+Map with the comment link configurations of the project. The name of
+the comment link configuration is mapped to the comment link
+configuration, which has the same format as the
+link:config-gerrit.html#_a_id_commentlink_a_section_commentlink[
+commentlink section] of `gerrit.config`.
+|`theme` |optional|
+The theme that is configured for the project as a link:#theme-info[
+ThemeInfo] entity.
+|=========================================
+
+[[config-input]]
+ConfigInput
+~~~~~~~~~~~
+The `ConfigInput` entity describes a new project configuration.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=========================================
+|Field Name ||Description
+|`description` |optional|
+The new description of the project. +
+If not set, the description is removed.
+|`use_contributor_agreements`|optional|
+Whether authors must complete a contributor agreement on the site
+before pushing any commits or changes to this project. +
+Can be `TRUE`, `FALSE` or `INHERIT`. +
+If not set, this setting is not updated.
+|`use_content_merge` |optional|
+Whether Gerrit will try to perform a 3-way merge of text file content
+when a file has been modified by both the destination branch and the
+change being submitted. This option only takes effect if submit type is
+not FAST_FORWARD_ONLY. +
+Can be `TRUE`, `FALSE` or `INHERIT`. +
+If not set, this setting is not updated.
+|`use_signed_off_by` |optional|
+Whether each change must contain a Signed-off-by line from either the
+author or the uploader in the commit message. +
+Can be `TRUE`, `FALSE` or `INHERIT`. +
+If not set, this setting is not updated.
+|`require_change_id` |optional|
+Whether a valid link:user-changeid.html[Change-Id] footer in any commit
+uploaded for review is required. This does not apply to commits pushed
+directly to a branch or tag. +
+Can be `TRUE`, `FALSE` or `INHERIT`. +
+If not set, this setting is not updated.
+|`max_object_size_limit` |optional|
+The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
+limit] of this project as a link:#max-object-size-limit-info[
+MaxObjectSizeLimitInfo] entity. +
+If set to `0`, the max object size limit is removed. +
+If not set, this setting is not updated.
+|`submit_type` |optional|
+The default submit type of the project, can be `MERGE_IF_NECESSARY`,
+`FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or
+`CHERRY_PICK`. +
+If not set, the submit type is not updated.
+|`state` |optional|
+The state of the project, can be `ACTIVE`, `READ_ONLY` or `HIDDEN`. +
+Not set if the project state is `ACTIVE`. +
+If not set, the project state is not updated.
+|=========================================
[[dashboard-info]]
DashboardInfo
@@ -881,6 +1336,29 @@
Not set if there is no parent.
|================================
+[[max-object-size-limit-info]]
+MaxObjectSizeLimitInfo
+~~~~~~~~~~~~~~~~~~~~~~
+The `MaxObjectSizeLimitInfo` entity contains information about the
+link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
+limit] of a project.
+
+[options="header",width="50%",cols="1,^2,4"]
+|===============================
+|Field Name ||Description
+|`value` |optional|
+The effective value of the max object size limit as a formatted string. +
+Not set if there is no limit for the object size.
+|`configured_value`|optional|
+The max object size limit that is configured on the project as a
+formatted string. +
+Not set if there is no limit for the object size configured on project
+level.
+|`inherited_value` |optional|
+The max object size limit that is inherited as a formatted string. +
+Not set if there is no global limit for the object size.
+|===============================
+
[[project-description-input]]
ProjectDescriptionInput
~~~~~~~~~~~~~~~~~~~~~~~
@@ -967,6 +1445,9 @@
|`require_change_id` |`INHERIT` if not set|
Whether the usage of Change-Ids is required for the project (`TRUE`,
`FALSE`, `INHERIT`).
+|`max_object_size_limit` |optional|
+Max allowed Git object size for this project.
+Common unit suffixes of 'k', 'm', or 'g' are supported.
|=========================================
[[project-parent-input]]
@@ -1002,6 +1483,22 @@
|`size_of_packed_objects` |Size of packed objects in bytes.
|======================================
+[[theme-info]]
+ThemeInfo
+~~~~~~~~~
+The `ThemeInfo` entity describes a theme.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=============================
+|Field Name ||Description
+|`css` |optional|
+The path to the `GerritSite.css` file.
+|`header` |optional|
+The path to the `GerritSiteHeader.html` file.
+|`footer` |optional|
+The path to the `GerritSiteFooter.html` file.
+|=============================
+
GERRIT
------
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index 303fc4b..7eed6ef 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -5,14 +5,22 @@
The API is suitable for automated tools to build upon, as well as
supporting some ad-hoc scripting use cases.
+See also: link:dev-rest-api.html[REST API Developers' Notes].
+
Endpoints
---------
+link:rest-api-access.html[/access/]::
+ Access Right related REST endpoints
link:rest-api-accounts.html[/accounts/]::
Account related REST endpoints
link:rest-api-changes.html[/changes/]::
Change related REST endpoints
+link:rest-api-config.html[/config/]::
+ Config related REST endpoints
link:rest-api-groups.html[/groups/]::
Group related REST endpoints
+link:rest-api-plugins.html[/plugins/]::
+ Plugin related REST endpoints
link:rest-api-projects.html[/projects/]::
Project related REST endpoints
@@ -53,9 +61,11 @@
----
JSON responses are encoded using UTF-8 and use content type
-`application/json`. The JSON response body starts with a magic prefix
-line that must be stripped before feeding the rest of the response
-body to a JSON parser:
+`application/json`.
+
+To prevent against Cross Site Script Inclusion (XSSI) attacks, the JSON
+response body starts with a magic prefix line that must be stripped before
+feeding the rest of the response body to a JSON parser:
----
)]}'
diff --git a/Documentation/user-changeid.txt b/Documentation/user-changeid.txt
index a4224bd..c13faa6 100644
--- a/Documentation/user-changeid.txt
+++ b/Documentation/user-changeid.txt
@@ -53,7 +53,7 @@
$ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg .git/hooks/
- $ curl -o .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
+ $ curl -Lo .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
Then ensure that the execute bit is set on the hook script:
diff --git a/Documentation/user-dashboards.txt b/Documentation/user-dashboards.txt
index 3a12b76..0945153 100644
--- a/Documentation/user-dashboards.txt
+++ b/Documentation/user-dashboards.txt
@@ -44,7 +44,7 @@
`&` or `;` or `,`
The special `foreach=...` parameter is designed to facilitate
-more easily writting similar queries in a dashboard. The value of the
+more easily writing similar queries in a dashboard. The value of the
foreach parameter will be used in every query in the dashboard by
appending it to their ends with a space (ANDing it with the queries).
@@ -66,7 +66,7 @@
will be used as name (equivalent to a title in a custom dashboard) for
the dashboard.
-Example dashboard config file `MyProject Dashboard`:
+Example of a dashboard config file:
----
[dashboard]
@@ -79,6 +79,8 @@
Once defined, project dashboards are accessible using stable URLs by
using the project name, refname and pathname of the dashboard via URLs
+, e.g. create a dashboard config file named `Main` and push it
+to `refs/meta/dashboards/Site` branch of All-Projects, then access it
like:
----
/#/projects/All-Projects,dashboards/Site:Main
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
index 711d1ac..558dcb5 100644
--- a/Documentation/user-notify.txt
+++ b/Documentation/user-notify.txt
@@ -22,6 +22,14 @@
change notifications to specific subsets, for example `branch:master`
to only see changes proposed for the master branch.
+Notification mails for new changes and new patch sets are not sent to
+the change owner.
+
+Notification mails for comments added on changes are not sent to the user
+who added the comment unless the user has enabled the 'CC Me On Comments I
+Write' option in the user preferences.
+
+
Project Level Settings
----------------------
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 3d72732..f1c13f8 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -168,6 +168,11 @@
+
Changes that match 'MESSAGE' arbitrary string in the commit message body.
+[[comment]]
+comment:'TEXT'::
++
+Changes that match 'TEXT' string in any comment left by a reviewer.
+
[[file]]
file:^'REGEX'::
+
@@ -187,12 +192,6 @@
ones using a bracket expression). For example, to match all XML
files named like 'name1.xml', 'name2.xml', and 'name3.xml' use
`file:"^name[1-3].xml"`.
-+
-Currently this operator is only available on a watched project
-and may not be used in the search bar. The same holds true for web UI
-"My > Watched Changes", i. e. file:regex is used over the is:watched
-expression. It never produces any results, because the error message:
-"operator not permitted here: file:regex" is suppressed.
[[has]]
has:draft::
@@ -278,7 +277,7 @@
----------------
Operator values that are not bare words (roughly A-Z, a-z, 0-9, @,
-hypen, dot and underscore) must be quoted for the query parser.
+hyphen, dot and underscore) must be quoted for the query parser.
Quoting is accepted as either double quotes
(e.g. `message:"the value"`) or as matched
@@ -291,8 +290,8 @@
Unless otherwise specified, operators are joined using the `AND`
boolean operator, thereby restricting the search results.
-Parentheses can be used to force a particular precendence on complex
-operator expressions, otherwise OR has higher precendence than AND.
+Parentheses can be used to force a particular precedence on complex
+operator expressions, otherwise OR has higher precedence than AND.
Negation
~~~~~~~~
@@ -311,7 +310,7 @@
OR
~~
The boolean operator `OR` (in all caps) can be used to find changes
-that match either operator. This increases the nubmer of results
+that match either operator. This increases the number of results
that are returned, as more changes are considered.
@@ -330,6 +329,12 @@
* The one or two character abbreviation shown in the column header
of change list pages. Example: `label:R` or `label:V`.
+* The label name followed by a ',' followed by a reviewer id or a
+ group id. To make it clear whether a user or group is being looked
+ for, precede the value by a user or group argument identifier
+ ('user=' or 'group='). If an LDAP group is being referenced make
+ sure to use 'ldap/<groupname>'.
+
A label name must be followed by a score, or an operator and a score.
The easiest way to explain this is by example.
@@ -356,7 +361,20 @@
+
Matches changes with either a +1, +2, or any higher score.
-`label:Code-Review<=-1`::
+`label:CodeReview=+2,aname`::
++
+Matches changes with a +2 code review where the reviewer or group is aname.
+
+`label:CodeReview=2,user=jsmith`::
++
+Matches changes with a +2 code review where the reviewer is jsmith.
+
+`label:CodeReview=+1,group=ldap/linux.workflow`::
++
+Matches changes with a +1 code review where the reviewer is in the
+ldap/linux.workflow group.
+
+`label:CodeReview<=-1`::
+
Matches changes with either a -1, -2, or any lower score.
diff --git a/Documentation/user-signedoffby.txt b/Documentation/user-signedoffby.txt
index d07516a..56858bf 100644
--- a/Documentation/user-signedoffby.txt
+++ b/Documentation/user-signedoffby.txt
@@ -104,10 +104,10 @@
mergers will sometimes manually convert an acker's "yep, looks good to me"
into an Acked-by:.
-Acked-by: does not necessarily indicate acknowledgement of the entire patch.
+Acked-by: does not necessarily indicate acknowledgment of the entire patch.
For example, if a patch affects multiple subsystems and has an Acked-by: from
-one subsystem maintainer then this usually indicates acknowledgement of just
-the part which affects that maintainer's code. Judgement should be used here.
+one subsystem maintainer then this usually indicates acknowledgment of just
+the part which affects that maintainer's code. Judgment should be used here.
When in doubt people should refer to the original discussion in the mailing
list archives.
diff --git a/ReleaseNotes/ReleaseNotes-2.0.12.txt b/ReleaseNotes/ReleaseNotes-2.0.12.txt
index bc3d90d..c82bab0 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.12.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.12.txt
@@ -110,7 +110,7 @@
* Delete dead CSS bundle code
* Always use NpTextBox or NpTextArea to prevent GlobalKe...
* Detect cases where system_config has too many rows
-* Remove unnecessary warning supressions
+* Remove unnecessary warning suppressions
* Remove dead code, these aren't used anymore
* Fix warnings about potential serialization problems
* Fix warning about debug code in OpenIdServiceImpl
diff --git a/ReleaseNotes/ReleaseNotes-2.0.18.txt b/ReleaseNotes/ReleaseNotes-2.0.18.txt
index 8c3e44b..303690c 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.18.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.18.txt
@@ -117,7 +117,7 @@
---------
* GERRIT-5 Remove PatchSetInfo from database and get it always fr...
+
-A very, very old bug. We no longer mirror the commit data into the SQL database, but instead pull it directly from Git when needed. Removing duplicated data simplifies the data store model, something that is important as we shift from a SQL database to a Git backed database.
+A very, very old bug. We no longer mirror the commit data into the SQL database, but instead pull it directly from Git when needed. Removing duplicated data simplifies the data store model, something that is important as we shift from an SQL database to a Git backed database.
* GERRIT-220 Fix infinite loop in PatchScriptBuilder
+
@@ -314,4 +314,4 @@
* Cleanup display of external ids in the user settings
* Preserve negative approvals when replacing patch sets
* Use gwtjsonrpc 1.1.1
-* gerrit 2.0.18
\ No newline at end of file
+* gerrit 2.0.18
diff --git a/ReleaseNotes/ReleaseNotes-2.0.19.txt b/ReleaseNotes/ReleaseNotes-2.0.19.txt
index 40dd390..b6a731b4 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.19.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.19.txt
@@ -149,7 +149,7 @@
Uploading commits to a project now requires that the new commits
share a common ancestry with the existing commits of that project.
This catches and prevents problems caused by a user making a typo
-in the project name, and inadverently selecting the wrong project.
+in the project name, and inadvertently selecting the wrong project.
* Change-Id tags in commit messages to associate commits
+
diff --git a/ReleaseNotes/ReleaseNotes-2.0.21.txt b/ReleaseNotes/ReleaseNotes-2.0.21.txt
index 47ba654..053ec89 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.21.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.21.txt
@@ -190,7 +190,7 @@
* Display abbreviated hexy Change-Id in screen titles
* Use hexy Change-Id in emails sent from Gerrit
+
-Change-Id abbreviations are now used throught more of the UI,
+Change-Id abbreviations are now used through more of the UI,
including emails sent by Gerrit and window/page titles. This
change breaks email threading for any existing review emails.
That is comments on a change created before the upgrade will
diff --git a/ReleaseNotes/ReleaseNotes-2.0.22.txt b/ReleaseNotes/ReleaseNotes-2.0.22.txt
index d27fcff..ae6d7dd 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.22.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.22.txt
@@ -60,7 +60,7 @@
The following properties may now be configured from LDAP using
more complex expressions: accountFullName, accountEmailAddress,
accountSshUserName. Property expressions permit forcing
-to a lowercase string, or performing string concentation.
+to a lowercase string, or performing string concatenation.
These features may help some environments to better integrate
with their local LDAP server.
@@ -118,7 +118,7 @@
oddball characters like '\0' and '/'. We really want them to
be a restricted subset which won't cause errors when we try to
map SSH usernames as file names in a Git repository as we try
-to move away from a SQL database.
+to move away from an SQL database.
* GERRIT-282 Fix reply to comment on left side
+
@@ -157,4 +157,4 @@
* GERRIT-67 Wait for dependencies to submit before claiming merge ...
* Move abandonChange to ChangeManageService
* Remove trailing whitespace in install.txt
-* Gerrit 2.0.22
\ No newline at end of file
+* Gerrit 2.0.22
diff --git a/ReleaseNotes/ReleaseNotes-2.0.5.txt b/ReleaseNotes/ReleaseNotes-2.0.5.txt
index a17d1c5..78389fc 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.5.txt
@@ -52,7 +52,7 @@
* Show the Web Identities panel when on HTTP authentication
* Relabel the "Web Identities" tab as just "Identities
* Use an when showing an empty cell in the identity...
-* Simplify the Gerrit install from source procedue to avoi...
+* Simplify the Gerrit install from source procedure to avoi...
* Support -DgwtStyle=DETAILED to support browser debugging
* Don't link to JIRA in our docs, link to our issues page
* Use in the identities table email column when emp...
diff --git a/ReleaseNotes/ReleaseNotes-2.1.1.txt b/ReleaseNotes/ReleaseNotes-2.1.1.txt
index 5d14b1e..5f02146 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.1.txt
@@ -102,8 +102,8 @@
* Use a glass pane behind our dialogs, make most modal
+
-Error dialogs are now more noticable, and less easily dismissed
-by an accidential click. This is especially useful when there
+Error dialogs are now more noticeable, and less easily dismissed
+by an accidental click. This is especially useful when there
is a merge error during submit.
Bug Fixes
diff --git a/ReleaseNotes/ReleaseNotes-2.1.10.txt b/ReleaseNotes/ReleaseNotes-2.1.10.txt
new file mode 100644
index 0000000..9d43bf4
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.1.10.txt
@@ -0,0 +1,13 @@
+Release notes for Gerrit 2.1.10
+===============================
+
+There are no schema changes from link:ReleaseNotes-2.1.9.html[2.1.9].
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.1.10.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.1.10.war]
+
+Bug Fixes
+---------
+* Fix clone for modern Git clients
++
+The security fix in 2.1.9 broke clone for recent Git clients,
+throwing an ArrayIndexOutOfBoundsException. Fixed.
diff --git a/ReleaseNotes/ReleaseNotes-2.1.2.txt b/ReleaseNotes/ReleaseNotes-2.1.2.txt
index 8652db7..ca69e86 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.2.txt
@@ -195,7 +195,7 @@
* issue 392 Make hooks/commit-msg available over HTTP
+
The scp filesystem holding client side tools and hooks is now
-avaliable over `http://review.example.com/tools/'$name'`. User
+available over `http://review.example.com/tools/'$name'`. User
documentation is updated with example URLs.
* issue 470 Allow /r/I... URLs
@@ -252,7 +252,7 @@
+
Additional public keys for the magical 'Gerrit Code Review' user may
be specified in an OpenSSH authorized_keys style file and are
-functionally equivilent to authenticating with the daemon's host key.
+functionally equivalent to authenticating with the daemon's host key.
The keys are primarily intended to be other daemons, most likely
slaves, that share the same set of repositories and database.
@@ -434,7 +434,7 @@
* issue 466 Reject pushing to invalid reference names
+
Gerrit allowed the invalid `HEAD:/refs/for/master` push refspec
-to actaully create the branch `refs/heads/refs/for/master`, which
+to actually create the branch `refs/heads/refs/for/master`, which
confused any other client trying to push. Fixed.
* issue 485 Trim the username before requesting authentication
@@ -467,7 +467,7 @@
* issue 451 gerrit.sh: Wait until the daemon is serving requests
+
-The gerrit.sh script now waits until the deamon is actually running
+The gerrit.sh script now waits until the daemon is actually running
and able to serve requests before returning to the caller with a
successful exit status code. This makes it easier to then start up
dependent tasks that need the server to be ready before they can run.
diff --git a/ReleaseNotes/ReleaseNotes-2.1.4.txt b/ReleaseNotes/ReleaseNotes-2.1.4.txt
index d4cf741..639229a 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.4.txt
@@ -21,7 +21,7 @@
* issue 504 Implement full query operators
+
-The search box now implments a wide range of operators and boolean
+The search box now implements a wide range of operators and boolean
expressions, permitting complex queries such as `is:open CodeReview>=1
(has:draft OR is:starred)` to locate open changes that have been code
reviewed, but still have unpublished drafts or were starred by the
diff --git a/ReleaseNotes/ReleaseNotes-2.1.5.txt b/ReleaseNotes/ReleaseNotes-2.1.5.txt
index c87dc42..b55aedf 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.5.txt
@@ -5,7 +5,7 @@
link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.5.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.5.war]
-This is primarly a bug fix release to 2.1.4, but some additional
+This is primarily a bug fix release to 2.1.4, but some additional
new features were included so its named 2.1.5 rather than 2.1.4.1.
Upgrade Instructions
@@ -156,7 +156,7 @@
if no new topic was supplied. If a new topic is supplied, it is
changed to match the new topic given.
-* Allow ; and & to seperate parameters in gitweb
+* Allow ; and & to separate parameters in gitweb
+
gitweb.cgi accepts either ';' or '&' between parameters, but
Gerrit Code Review was only accepting the ';' syntax. Fixed
diff --git a/ReleaseNotes/ReleaseNotes-2.1.6.txt b/ReleaseNotes/ReleaseNotes-2.1.6.txt
index 198a064..d1c6335 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.6.txt
@@ -89,7 +89,7 @@
embedded Jetty web server with SSL enabled, and an LDAP directory to
lookup individual account information.
-* issue 503 Inactive acounts may be disabled.
+* issue 503 Inactive accounts may be disabled.
+
Administrators can manually update the accounts table, setting
inactive = `Y` to mark user accounts inactive. Inactive accounts
diff --git a/ReleaseNotes/ReleaseNotes-2.1.7.2.txt b/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
index 9172b4a..802f1c7 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
@@ -25,7 +25,7 @@
* Fix API breakage on ChangeDetailService
+
Version 2.1.7 broke the Gerrit Code Review plugin for Mylyn Reviews
-due to an accidential signature change of one of the remote JSON
+due to an accidental signature change of one of the remote JSON
APIs. The ChangeDetailService.patchSetDetail() method is back to the
old signature and a new patchSetDetail2() method has been added to
handle the newer calling convention used in some contexts of the
diff --git a/ReleaseNotes/ReleaseNotes-2.1.7.txt b/ReleaseNotes/ReleaseNotes-2.1.7.txt
index 0dba5f9..b12713c 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.7.txt
@@ -71,7 +71,7 @@
that differ between two patch sets. This new feature can speed up
re-reviewing a change.
-* issue 913 Support different color pallete when not signed in
+* issue 913 Support different color palette when not signed in
+
Site administrators can configure a different theme in gerrit.config for
the signed-in and signed-out states, making it more obvious to site users
@@ -328,7 +328,7 @@
* Reject invalid Change-Id lines
+
-Severly malformed Change-Id lines were previously accepted by the
+Severely malformed Change-Id lines were previously accepted by the
server. These are now rejected.
* Fix error message returned on push to closed change
@@ -362,7 +362,7 @@
* issue 814 Evict initial members of group created by SSH
* issue 879 Fix replication of initial empty commit in new project
* Disallow setting a project as parent for itself
-* Autoamtically create user account(s) as necessary
+* Automatically create user account(s) as necessary
* Move SSH command creation off NioProcessor threads
Administration
diff --git a/ReleaseNotes/ReleaseNotes-2.1.9.txt b/ReleaseNotes/ReleaseNotes-2.1.9.txt
new file mode 100644
index 0000000..728d7cc
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.1.9.txt
@@ -0,0 +1,16 @@
+Release notes for Gerrit 2.1.9
+==============================
+
+There are no schema changes from link:ReleaseNotes-2.1.8.html[2.1.8].
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.1.9.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.1.9.war]
+
+Bug Fixes
+---------
+* Patch JGit security hole
++
+The security hole may permit a modified Git client to gain access
+to hidden or deleted branches if the user has read permission on
+at least one branch in the repository. Access requires knowing a
+SHA-1 to request, which may be discovered out-of-band from an issue
+tracker or gitweb instance.
diff --git a/ReleaseNotes/ReleaseNotes-2.2.1.txt b/ReleaseNotes/ReleaseNotes-2.2.1.txt
index 19af06d..ff8040e 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.1.txt
@@ -35,7 +35,7 @@
The name "-- All Projects --.git" is difficult to work with on
the UNIX command line, due to tools assuming the name is actually
part of a long option. The project has been renamed to remove these
-leading hypens, and remove spaces, making it more friendly to work
+leading hyphens, and remove spaces, making it more friendly to work
with on the command line.
* issue 997 Resolve Project Owners when checking access rights
@@ -66,7 +66,7 @@
* Fix API breakage on ChangeDetailService
+
Version 2.1.7 broke the Gerrit Code Review plugin for Mylyn Reviews
-due to an accidential signature change of one of the remote JSON
+due to an accidental signature change of one of the remote JSON
APIs. The ChangeDetailService.patchSetDetail() method is back to the
old signature and a new patchSetDetail2() method has been added to
handle the newer calling convention used in some contexts of the
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.2.txt b/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
index db5d750..f7c299e 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
@@ -21,4 +21,4 @@
section name as All-Projects. This is an unlikely scenario for
most servers, as Gerrit does not normally set inheritFrom equal to
All-Projects. The usual behavior is to not supply this property in
-project.config, and permit the implicit inheritence to take place.
+project.config, and permit the implicit inheritance to take place.
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.txt b/ReleaseNotes/ReleaseNotes-2.2.2.txt
index ddfe323..2c14fa1 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.txt
@@ -172,7 +172,7 @@
* Add a "Save" button to the PatchScriptSettingsPanel
+
-The "Update" button now only updates the display. Addittionally,
+The "Update" button now only updates the display. Additionally,
for logged in users, a "Save" button now behaves the way that
"Update" used to behave for logged in users.
@@ -427,9 +427,9 @@
* Refactor how permissions are matched by ProjectControl, RefControl
+
More aggressively cache many of the auth objects at a cost of memory,
-but this should be an improvement in response timse.
+but this should be an improvement in response times.
-* Substantialy speed up pushing changes for review
+* Substantially speed up pushing changes for review
+
Pushing a new change for review checks if the change is related to
the branch it's destined for. It used to do this in a way that
diff --git a/ReleaseNotes/ReleaseNotes-2.3.1.txt b/ReleaseNotes/ReleaseNotes-2.3.1.txt
index 324a3c1..c37fbe4 100644
--- a/ReleaseNotes/ReleaseNotes-2.3.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.3.1.txt
@@ -21,4 +21,4 @@
section name as All-Projects. This is an unlikely scenario for
most servers, as Gerrit does not normally set inheritFrom equal to
All-Projects. The usual behavior is to not supply this property in
-project.config, and permit the implicit inheritence to take place.
+project.config, and permit the implicit inheritance to take place.
diff --git a/ReleaseNotes/ReleaseNotes-2.3.txt b/ReleaseNotes/ReleaseNotes-2.3.txt
index 965c8e3..cf371c8 100644
--- a/ReleaseNotes/ReleaseNotes-2.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.3.txt
@@ -220,7 +220,7 @@
This helps prevent a very slow LDAP server from blocking
all SSH command creation threads.
-* Introduce a git maxObjectSizeLimit in the [recieve] config
+* Introduce a git maxObjectSizeLimit in the [receive] config
+
This limits the size of uploaded files
@@ -294,7 +294,7 @@
* Allow Realm to participate when linking an account identity
+
-When linking a new user identity to an exisiting account, permit the
+When linking a new user identity to an existing account, permit the
Realm to observe the new incoming identity and the current account,
and to alter the request. This enables a Realm to observe when a
user verifies a new email address link.
@@ -401,7 +401,7 @@
If a project inherits from a non existing parent, prevent a
StringIndexOutOfBoundsException.
-* Fix: Supress "Error on refs/cache-automerge" warnings.
+* Fix: Suppress "Error on refs/cache-automerge" warnings.
* Don't allow registering for cleanup after cleanup runs
+
diff --git a/ReleaseNotes/ReleaseNotes-2.4.2.txt b/ReleaseNotes/ReleaseNotes-2.4.2.txt
index afa1d96..9ae9fd7 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.2.txt
@@ -21,4 +21,4 @@
section name as All-Projects. This is an unlikely scenario for
most servers, as Gerrit does not normally set inheritFrom equal to
All-Projects. The usual behavior is to not supply this property in
-project.config, and permit the implicit inheritence to take place.
+project.config, and permit the implicit inheritance to take place.
diff --git a/ReleaseNotes/ReleaseNotes-2.4.3.txt b/ReleaseNotes/ReleaseNotes-2.4.3.txt
new file mode 100644
index 0000000..c9c2d2c
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.4.3.txt
@@ -0,0 +1,16 @@
+Release notes for Gerrit 2.4.3
+==============================
+
+There are no schema changes from link:ReleaseNotes-2.4.2.html[2.4.2].
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.4.3.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.4.3.war]
+
+Bug Fixes
+---------
+* Patch JGit security hole
++
+The security hole may permit a modified Git client to gain access
+to hidden or deleted branches if the user has read permission on
+at least one branch in the repository. Access requires knowing a
+SHA-1 to request, which may be discovered out-of-band from an issue
+tracker or gitweb instance.
diff --git a/ReleaseNotes/ReleaseNotes-2.4.4.txt b/ReleaseNotes/ReleaseNotes-2.4.4.txt
new file mode 100644
index 0000000..de9e2cb
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.4.4.txt
@@ -0,0 +1,13 @@
+Release notes for Gerrit 2.4.4
+==============================
+
+There are no schema changes from link:ReleaseNotes-2.4.4.html[2.4.4].
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.4.4.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.4.4.war]
+
+Bug Fixes
+---------
+* Fix clone for modern Git clients
++
+The security fix in 2.4.3 broke clone for recent Git clients,
+throwing an ArrayIndexOutOfBoundsException. Fixed.
diff --git a/ReleaseNotes/ReleaseNotes-2.4.txt b/ReleaseNotes/ReleaseNotes-2.4.txt
index 82f3ed4..6f10c0b 100644
--- a/ReleaseNotes/ReleaseNotes-2.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.4.txt
@@ -131,7 +131,7 @@
'refs/heads/' prefix). Change the branch operator so that it also
supports full branch names as value.
+
-It is intuive that searching with 'branch:master' and searching with
+It is intuitive that searching with 'branch:master' and searching with
'branch:refs/for/master' deliver the same result. So far
'branch:refs/for/master' was the same as searching with
'refs:refs/heads/refs/heads/master' which is unexpected for most users.
@@ -154,7 +154,7 @@
* issue 1272 Add scripts to create release notes from git log
+
These script generates a list of commits from git log between two given commits
-and outputs the asciidoc format containting list of commits subject and body.
+and outputs the asciidoc format containing list of commits subject and body.
* Update URL for m2eclipse
+
@@ -223,7 +223,7 @@
* issue 1353 Fix case check for project name so that symlinks work again
* Fix merging of access sections
-* Fix inconsistent behaviour when replicating refs/meta/config
+* Fix inconsistent behavior when replicating refs/meta/config
* Fix duplicated results on status:open project:P branch:B
Documentation
diff --git a/ReleaseNotes/ReleaseNotes-2.5.2.txt b/ReleaseNotes/ReleaseNotes-2.5.2.txt
index 08b4f86..f87328e 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.2.txt
@@ -124,9 +124,9 @@
the 'Uploading Changes' documentation was broken.
* link:http://code.google.com/p/gerrit/issues/detail?id=1569[issue 1569]:
-Fix unexpected behaviour in the commit-msg hook caused by `GREP_OPTIONS`
+Fix unexpected behavior in the commit-msg hook caused by `GREP_OPTIONS`
+
-If `GREP_OPTIONS` was set, it caused unexpected behaviour in the
+If `GREP_OPTIONS` was set, it caused unexpected behavior in the
commit-msg hook. For example if it included a setting like
`--exclude=".git/*"` it caused a new `Change-Id` line to be appended
to the commit message on every amend.
diff --git a/ReleaseNotes/ReleaseNotes-2.5.5.txt b/ReleaseNotes/ReleaseNotes-2.5.5.txt
new file mode 100644
index 0000000..57b6d24
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.5.txt
@@ -0,0 +1,16 @@
+Release notes for Gerrit 2.5.5
+==============================
+
+There are no schema changes from link:ReleaseNotes-2.5.4.html[2.5.4].
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.5.5.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.5.5.war]
+
+Bug Fixes
+---------
+* Patch JGit security hole
++
+The security hole may permit a modified Git client to gain access
+to hidden or deleted branches if the user has read permission on
+at least one branch in the repository. Access requires knowing a
+SHA-1 to request, which may be discovered out-of-band from an issue
+tracker or gitweb instance.
diff --git a/ReleaseNotes/ReleaseNotes-2.5.6.txt b/ReleaseNotes/ReleaseNotes-2.5.6.txt
new file mode 100644
index 0000000..2e9e888
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.6.txt
@@ -0,0 +1,13 @@
+Release notes for Gerrit 2.5.6
+==============================
+
+There are no schema changes from link:ReleaseNotes-2.5.6.html[2.5.6].
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.5.6.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.5.6.war]
+
+Bug Fixes
+---------
+* Fix clone for modern Git clients
++
+The security fix in 2.5.4 broke clone for recent Git clients,
+throwing an ArrayIndexOutOfBoundsException. Fixed.
diff --git a/ReleaseNotes/ReleaseNotes-2.5.txt b/ReleaseNotes/ReleaseNotes-2.5.txt
index 023b4bf..cdef554 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.txt
@@ -80,7 +80,7 @@
configuration variables are now expressed in bytes of memory used,
rather than number of entries in the cache. This is a change from
previous versions of Gerrit and gives administrators more control over
-how memory is partioned within a server. Admins that set this variable
+how memory is partitioned within a server. Admins that set this variable
must update their configurations, as the old values are too small.
For example a setting of `memoryLimit = 1024` now means only 1 KiB of
data (which may not even hold 1 patch set), not 1024 patch sets. It
@@ -1014,7 +1014,7 @@
code wants to use one of these caches during startup to resolve a
group or project by name.
+
-Tracking the Gauva backend caches with a DynamicMap makes it possible
+Tracking the Guava backend caches with a DynamicMap makes it possible
for plugins to define their own in-memory caches using CacheModule's
cache() function to declare the cache. It allows the core server to
make the cache available to administrators over SSH with the gerrit
@@ -1129,7 +1129,7 @@
* Add change topic in hook arguments
+
It was not possible for hook scripts to include topic-specific
-behaviour because the topic name was not included in the arguments.
+behavior because the topic name was not included in the arguments.
* Add `--is-draft` argument on `patchset-created` hook
+
@@ -1202,7 +1202,7 @@
* Set `GERRIT_SITE` in Gerrit hooks as environment variable
+
-Allows development of hooks parametrised on Gerrit location. This can
+Allows development of hooks parameterized on Gerrit location. This can
be useful to allow hooks to load the Gerrit configuration when needed
(from `$GERRIT_SITE`) or even store their additional config files under
`$GERRIT_SITE/etc` and retrieve them at startup.
@@ -1210,7 +1210,7 @@
* Add an exponentially rolling garbage collection script
+
`git-exproll.sh` is a git garbage collection script aimed specifically
-at reducing exccessive garbage collection and particularly large
+at reducing excessive garbage collection and particularly large
packfile churn for Gerrit installations.
+
Excessive garbage collection on "dormant" repos is wasteful of both CPU
@@ -1275,7 +1275,7 @@
just the list of groups.
+
Now the amount of data that needs to be downloaded to the browser is
-reduced by using the more leightweight `AccountGroup` type instead of
+reduced by using the more lightweight `AccountGroup` type instead of
the `GroupDetail` type when showing the groups in a list format. As a
consequence the `Owners` column that showed the name of the owner group
had been dropped.
@@ -1305,9 +1305,9 @@
used in the `refs/for/` push line.
+
Catch existing changes by looking for their exact commit SHA-1, rather
-than complete ancestory. This should have roughly the same outcome for
+than complete ancestry. This should have roughly the same outcome for
anyone pushing a new commit on top of an existing open change, but
-with lower computional cost at the server.
+with lower computational cost at the server.
* Lookup changes in parallel during `ReceiveCommits`
+
@@ -1408,7 +1408,7 @@
* Use gwtexpui 1.2.6
+
** Hide superfluous status text from clippy flash widget
-** Fix diappearance of text in CopyableLabel when clicking on it
+** Fix disappearance of text in CopyableLabel when clicking on it
* Update Guava to 12.0.1
+
@@ -1436,7 +1436,7 @@
is assigned) are able to edit the access control list of the projects
they own. Hence being owner of the `All-Projects` project would allow
to edit the global capabilities and assign the `administrateServer`
-capabilitiy without being Gerrit administrator.
+capability without being Gerrit administrator.
+
In earlier Gerrit versions (2.1.x) it was already implemented like
this but the corresponding checks got lost.
@@ -1503,7 +1503,7 @@
* Dependencies were lost in the ChangeScreen's "Needed By" table
+
-Older patchsets are now iterated for decendents, so that the dependency
+Older patchsets are now iterated for descendants, so that the dependency
chain does not break on new upstream patchsets.
* link:http://code.google.com/p/gerrit/issues/detail?id=1442[issue 1442]:
@@ -1712,7 +1712,7 @@
* Fix the `export-review-notes` command's Guice bindings
+
-The `export-review-notes` command was broken becasue of the CachePool
+The `export-review-notes` command was broken because of the CachePool
class being bound twice. The startup of the command failed because of
that.
@@ -1751,7 +1751,7 @@
Replication
~~~~~~~~~~~
-* Fix inconsistent behaviour when replicating `refs/meta/config`
+* Fix inconsistent behavior when replicating `refs/meta/config`
+
In `replication.config`, if `authGroup` is set to be used together with
`mirror = true`, refs blocked through the `authGroup` are deleted from
diff --git a/ReleaseNotes/ReleaseNotes-2.6.1.txt b/ReleaseNotes/ReleaseNotes-2.6.1.txt
new file mode 100644
index 0000000..e163b43d
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.6.1.txt
@@ -0,0 +1,16 @@
+Release notes for Gerrit 2.6.1
+==============================
+
+There are no schema changes from link:ReleaseNotes-2.6.html[2.6].
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.6.1.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.6.1.war]
+
+Bug Fixes
+---------
+* Patch JGit security hole
++
+The security hole may permit a modified Git client to gain access
+to hidden or deleted branches if the user has read permission on
+at least one branch in the repository. Access requires knowing a
+SHA-1 to request, which may be discovered out-of-band from an issue
+tracker or gitweb instance.
diff --git a/ReleaseNotes/ReleaseNotes-2.6.2.txt b/ReleaseNotes/ReleaseNotes-2.6.2.txt
index cf963c2..af00a71 100644
--- a/ReleaseNotes/ReleaseNotes-2.6.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.6.2.txt
@@ -21,10 +21,6 @@
If the title is not specified, the path of the dashboard config file
is used as title.
-* link:https://code.google.com/p/gerrit/issues/detail?id=2010[Issue 2010]:
-Fix null-pointer exception when searching for changes with the query
-`owner:self`.
-
* Properly handle double-click on external group in GroupTable.
+
Double-clicking on an external group opens the group's URL (if it
@@ -38,6 +34,34 @@
* Allow label values to be configured with no text.
+* link:https://code.google.com/p/gerrit/issues/detail?id=1966[Issue 1966]:
+Fix Gerrit plugins under Tomcat by avoiding Guice static filter.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2010[Issue 2010]:
+Fix null-pointer exception when searching for changes with the query
+`owner:self`.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2039[Issue 2039]:
+Fix browser null-pointer exception when ChangeCache is incomplete.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2054[Issue 2054]:
+Expand capabilities of `ldap.groupMemberPattern`.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2056[Issue 2056]:
+Display custom NoOp label score for open changes.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2093[Issue 2093]:
+Fix incorrect title of "repo download" link on change screen.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2098[Issue 2098]:
+Fix re-enabling of disabled plugins.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2127[Issue 2127]:
+Remove hard-coded documentation links from the admin page.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2128[Issue 2128]:
+Fix null-pointer exception when deleting draft patch set when previous
+draft was already deleted.
No other changes since 2.6.1.
diff --git a/ReleaseNotes/ReleaseNotes-2.6.txt b/ReleaseNotes/ReleaseNotes-2.6.txt
index 0c21182..a000a62 100644
--- a/ReleaseNotes/ReleaseNotes-2.6.txt
+++ b/ReleaseNotes/ReleaseNotes-2.6.txt
@@ -3,7 +3,7 @@
Gerrit 2.6 is now available:
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.6.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.6.war]
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.6.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.6.war]
Gerrit 2.6 includes the bug fixes done with
link:ReleaseNotes-2.5.1.html[Gerrit 2.5.1],
@@ -439,7 +439,7 @@
+
Enable this on the REST API responses because we sometimes send back
text/plain results that are really just plain text. Existing JSON
-responses are protected from accidential sniffing and treatment as
+responses are protected from accidental sniffing and treatment as
HTML thanks to Gson encoding HTML control characters using Unicode
character escapes within JSON strings.
@@ -870,13 +870,13 @@
* PatchSet.isRef()-optimizations.
+
PatchSet.isRef() is used extensively when preparing for a ref
-advertisment and the regular expression used by isRefs() was notably
+advertisement and the regular expression used by isRefs() was notably
costly in these circumstances, especially since it could not be
pre-compiled.
+
The regular expression is removed and the check is now directly
implemented. As result the performance of `git ls-remote` could be
-increased upto 15%.
+increased by up to 15%.
* New config option `receive.checkReferencedObjectsAreReachable`
+
@@ -898,11 +898,11 @@
* Caching of changes
+
-During Ref Advertisments (via VisibleRefFilter), all changes need to
+During Ref Advertisements (via VisibleRefFilter), all changes need to
be fetched from the database to allow Gerrit to figure out which change
refs are visible and should be advertised to the user. To reduce
database traffic a cache for changes was introduced. This cache is
-disabled by default since it can mess-up multi-server setups.
+disabled by default since it can mess up multi-server setups.
Misc
~~~~
@@ -1140,13 +1140,13 @@
+
** Sometimes a second comment editor was shown instead of using the
existing comment editor.
-** Fix doublicated border line between comments.
+** Fix duplicated border line between comments.
** Sometimes the parts of the border were missing when a comment was
expanded.
** Fix displaying the blue line that marks the current line when there
are several published comments.
** Sometimes on discard of a comment some frames of the comment editor
- stayed and some border lines of neighbour comments disappeared.
+ stayed and some border lines of neighbor comments disappeared.
* In diff view don't let arrow column accept clicks.
@@ -1178,7 +1178,7 @@
+
The provided suggestions should highlight the part that the user has
already typed as bold text. This only worked for the first operator.
-For suggestions of any further operator no hightlighting was done.
+For suggestions of any further operator no highlighting was done.
* Fix style of hint text in search box on initial page load
+
@@ -1335,7 +1335,7 @@
branch doesn't exist anymore.
+
In addition Gerrit was writing an error into the error log if a change
-couldn't be merged because the destination branch wass missing.
+couldn't be merged because the destination branch was missing.
That was not really a server error and is not logged anymore.
* Fix NPE when pushing a patch with an invalid author with
@@ -1612,7 +1612,7 @@
servlet API in a new `WEB-INF/pgm-lib` directory.
* link:https://code.google.com/p/gerrit/issues/detail?id=1822[Issue 1822]:
- Verify session matches container authentiation header
+ Verify session matches container authentication header
+
If the user alters their identity in the container invalidate
the Gerrit user session and force a new one to begin.
diff --git a/ReleaseNotes/ReleaseNotes-2.7.txt b/ReleaseNotes/ReleaseNotes-2.7.txt
index dae075d..eb95146 100644
--- a/ReleaseNotes/ReleaseNotes-2.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.7.txt
@@ -4,8 +4,7 @@
Gerrit 2.7 is now available:
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.7.war[
-http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.7.war]
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.7-rc2.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.7-rc2.war]
Gerrit 2.7 includes the bug fixes done with
link:ReleaseNotes-2.6.1.html[Gerrit 2.6.1] and
@@ -27,6 +26,15 @@
+Gerrit Trigger Plugin in Jenkins
+--------------------------------
+
+
+*WARNING:* Upgrading to 2.7 may cause the Gerrit Trigger Plugin in Jenkins to
+stop working. Please see the "New 'Stream Events' global capability" section
+below.
+
+
Release Highlights
------------------
@@ -70,6 +78,10 @@
Stream Events capability] controls access to the `stream-events` ssh command.
+
Only administrators and users having this capability are allowed to use `stream-events`.
++
+If you are using the Gerrit Trigger Plugin in Jenkins, you must make sure that the
+'Non-Interactive Users' group, or whichever group the Jenkins user belongs to, is
+given the 'Stream Events' capability.
* Allow opening new changes on existing commits.
+
diff --git a/ReleaseNotes/ReleaseNotes-2.8.txt b/ReleaseNotes/ReleaseNotes-2.8.txt
new file mode 100644
index 0000000..1c63983f
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.8.txt
@@ -0,0 +1,522 @@
+Release notes for Gerrit 2.8
+============================
+
+
+Gerrit 2.8 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.8.war[
+http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.8.war]
+
+
+Schema Change
+-------------
+
+
+*WARNING:* This release contains schema changes. To upgrade:
+----
+ java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* Upgrading to 2.8.x requires the server be first upgraded to 2.1.7 (or
+a later 2.1.x version), and then to 2.8.x. If you are upgrading from 2.2.x.x or
+later, you may ignore this warning and upgrade directly to 2.8.x.
+
+
+Release Highlights
+------------------
+
+
+* Lots of new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api.html[
+REST API endpoints].
+
+* New link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/js-api.html[
+JavaScript API].
+
+* New build system using link:http://facebook.github.io/buck/[Facebook Buck].
+
+
+New Features
+------------
+
+
+Configuration
+~~~~~~~~~~~~~
+
+* Project owners can define `receive.maxObjectSizeLimit` in the
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/config-gerrit.html#receive.maxObjectSizeLimit[
+project configuration] to further reduce the global setting.
+
+* Site administrators can define a
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/config-mail.html#_footer_vm[
+footer template] that will be appended to the end of all outgoing emails after
+the 'ChangeFooter' and 'CommentFooter'.
+
+* New `topic-changed` hook and stream event is fired when a change's topic is
+edited from the Web UI or via a REST API.
+
+* New options `--list-plugins` and `--install-plugins` on the
+link:[http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/pgm-init.html[
+site initialization command].
+
+* New `auth.httpDisplaynameHeader` and `auth.httpEmailHeader` in the
+link:[http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/config_gerrit.html#_a_id_auth_a_section_auth[
+authentication configuration].
++
+When using HTTP-based authentication, the SSO can be delegated to check not only
+the user credentials but also to fetch the full user-profile.
++
+With the config properties `auth.httpDisplaynameHeader` and `auth.httpEmailHeader`
+it is possible to configure the name of the headers used for propagating this extra
+information and enforce them on the user profile during login and beyond.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/config_gerrit.html#_a_id_httpd_a_section_httpd[
+Customizable registration page for HTTP authentication].
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/config_gerrit.html#_a_id_httpd_a_section_httpd[
+Configurable external `robots.txt` file].
+
+* Support for
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/database-setup.html#createdb_oracle[
+Oracle database].
+
+Web UI
+~~~~~~
+
+
+Global
+^^^^^^
+
+* The change status is shown in a separate column on dashboards and search results.
+
+Change Screens
+^^^^^^^^^^^^^^
+
+
+* New button to cherry-pick the change to another branch.
+
+* When issuing a rebase via the Web UI, the committer is now the logged in
+ user, rather than "Gerrit Code Review".
++
+If the user has more than one email address, the preferred email address will
+be used.
+
+* Default user's full name to git committer name if user has not configured a
+full name in their profile.
+
+* Include comment author attributes in comment panels.
++
+Comment author's email address and name are included as attributes in comment
+panels. This makes it easier to filter out CI-based comments using user
+scripts.
+
+* Copy reviewed flag to new patch sets for identical files.
++
+If a user has already seen and reviewed a file, the 'reviewed' flag is forwarded
+on to the next patch set when the content of the file in the next patch set is
+identical to the reviewed file.
+
+
+REST API
+~~~~~~~~
+
+* Several new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api.html[
+REST API endpoints] are added.
+
+* REST views can determine how long their response should be cached.
+
+* REST views can handle 'HTTP 422 Unprocessable Entity' responses.
+
+Access Rights
+^^^^^^^^^^^^^
+
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-access.html#list-access[
+List access rights for project(s)]
+
+Accounts
+^^^^^^^^
+
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#create-account[
+Create account]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#get-account-name[
+Get account full name]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#set-account-name[
+Set account full name]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#delete-account-name[
+Delete account full name]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#list-account-emails[
+List account email addresses]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#get-account-email[
+Get account email address]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#set-preferred-email[
+Set account preferred email address]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#create-account-email[
+Create account email]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#delete-account-email[
+Delete account email]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#get-active[
+Get account state]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#set-active[
+Set account state to active]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#delete-active[
+Set account state to inactive]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#get-http-password[
+Get account HTTP password]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#set-http-password[
+Set or generate account HTTP password]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#delete-http-password[
+Delete account HTTP password]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#list-ssh-keys[
+List account SSH keys]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#get-ssh-key[
+Get account SSH key]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#add-ssh-key[
+Add account SSH key]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#delete-ssh-key[
+Delete account SSH key]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-accounts.html#get-username[
+Get account username]
+
+Changes
+^^^^^^^
+
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-changes.html#rebase-change[
+Rebase change]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-changes.html#cherry-pick[
+Cherry-pick revision]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-changes.html#get-content[
+Get content of a file in a revision]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-changes.html#get-patch[
+Get revision as a formatted patch]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-changes.html#get-diff[
+Get diff of a file in a revision]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-changes.html#get-commit[
+Get parsed commit of a revision]
+
+
+Config
+^^^^^^
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-config.html#get-capabilities[
+Get capabilities]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-config.html#get-version[
+Get version] (of the Gerrit server)
+
+
+Projects
+^^^^^^^^
+
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-projects.html#list-branches[
+List branches]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-projects.html#get-branch[
+Get branch]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-projects.html#create-branch[
+Create branch]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-projects.html#delete-branch[
+Delete branch]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-projects.html#list-child-projects[
+List child projects]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-projects.html#get-child-project[
+Get child project]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-projects.html#set-config[
+Set configuration]
+
+
+Capabilities
+~~~~~~~~~~~~
+
+
+New global capabilities are added.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/access_control.html#capability_generateHttpPassword[
+Generate Http Password] allows non-administrator users to generate HTTP
+passwords for users other than themselves.
++
+This capability would typically be assigned to a non-interactive group
+to be able to generate HTTP passwords for users from a tool or web service
+that uses the Gerrit REST API.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/access_control.html#capability_runAs[
+Run As] allows users to impersonate other users by setting the `X-Gerrit-RunAs`
+HTTP header on REST API calls.
++
+Site administrators do not inherit this capability; it must be granted
+explicitly.
+
+
+Emails
+~~~~~~
+
+* The `RebasedPatchSet` template is removed. Email notifications for rebased
+changes are now sent with the `ReplacePatchSet` template.
+
+* Comment notification emails now include context of comments that are replied
+to, and links to the file(s) in which comments are made.
+
+
+Plugins
+~~~~~~~
+
+
+Global
+^^^^^^
+
+
+* Plugins may now contribute buttons to various parts of the UI using the
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/js-api.html[
+JavaScript API].
+
+* Plugins may now provide an 'About' section on their documentation index page.
+
+* Plugins may now provide separate sections for REST API and servlet
+documentation on their index page.
+
+* Plugins may now provide
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/config-validation.html#pre-merge-validation[
+pre-merge validation steps].
+
+* Plugins may now provide
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/dev-plugins.html#capabilities[
+Global capabilities].
+
+* The "hello world" plugin is replaced with the "cookbook plugin" which has more
+examples of the plugin API's usage.
+
+
+Commit Message Length Checker
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+
+* Commits whose subject or body length exceeds the limit can be rejected.
+
+Replication
+^^^^^^^^^^^
+
+* The `{$name}` placeholder is optional when replicating a single project,
+allowing a single project to be replicated under a different name.
+
+* Project names can be matched with wildcard or regex patterns in `replication.config`.
+
+* The `replication start` command does not exit until replication is finished
+when the `--wait` option is used.
+
+* The `replication start` command displays a summary of the replication status.
+
+* Retry counts are added to replication task names, so they can be seen in the
+output of the `show-queue` command.
+
+* The `remoteNameStyle` option can be set to `basenameOnly` to replicate projects
+using only the basename on the target server.
+
+* The `startReplication` global capability is now provided by the plugin.
+
+ssh
+~~~
+
+
+* The `commit-msg` hook installation command is now
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/config_gerrit.html#gerrit.installCommitMsgHookCommand[
+configurable].
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/cmd-ls-members.html[
+New `ls-members` command].
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/cmd-set-members.html[
+New `set-members` command].
++
+New command to manipulate group membership. Members can be added or removed
+and groups can be included or excluded in one specific group or number of groups.
+
+* The full commit message is now included in the data sent by the
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/cmd-stream-events.html[
+`stream-events` command].
+
+* The link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/cmd-show-queue.html[
+`show-queue` command] now shows the time that a task was added to the queue.
+
+
+Daemon
+~~~~~~
+
+
+* Add `--init` option to Daemon to initialize site on daemon start.
++
+The `--init` option will also upgrade an already existing site and is processed in
+non-interactive (batch) mode.
+
+
+Bug Fixes
+---------
+
+
+General
+~~~~~~~
+
+
+* Use the parent change on the same branch for rebases.
++
+Since there can be multiple changes with the same commit on different branches,
+use the parent change on the same branch during rebase.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=600[Issue 600]:
+Fix change stuck in SUBMITTED state but actually merged.
+
+Configuration
+~~~~~~~~~~~~~
+
+
+* Do not persist default project state in `project.config`.
+
+* Honor the `gerrit.cannonicalWebUrl` setting when opening the browser after init.
+
+* Fix 'query disabled' error when Query Limit is set.
+
+* Honor the `gerrit.createChangeId` setting from the git config in the
+The link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/cmd-hook-commit-msg.html[
+`commit-msg` hook].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2045[Issue 2045]:
+Define user scope when parsing server config.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1990[Issue 1990]:
+Support optional Certificate Revocation List (CRL) with `CLIENT_SSL_CERT_LDAP`.
+
+Web UI
+~~~~~~
+
+
+Global
+^^^^^^
+
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1574[Issue 1574]:
+Correctly highlight matches of text in escaped HTML entities in suggestion results.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1996[Issue 1996]:
+The "Keyboard Shortcuts" help popup can be closed by pressing the Escape key.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2013[Issue 2013]:
+Correctly populate the list of watched changes when watching more than one project.
+
+Change Screens
+^^^^^^^^^^^^^^
+
+
+* Default review comment visibility is changed to expand all recent.
++
+By default all comments within the last week are expanded, rather than
+only the most recent.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1814[Issue 1814]:
+Sort labels alphabetically by name in the approval table.
+
+Project Screens
+^^^^^^^^^^^^^^^
+
+
+* Only enable the delete branch button when branches are selected.
+
+* Disable the delete branch button while branch deletion requests are
+still being processed.
+
+User Profile Screens
+^^^^^^^^^^^^^^^^^^^^
+
+
+* The preferred email address field is shown as empty if the user has no
+preferred email address.
+
+
+REST API
+~~~~~~~~
+
+
+* Support raw input also in POST requests.
+
+* Show granted date for labels/all when using `/changes/`.
+
+* Return all revisions when `o=ALL_REVISIONS` is set on `/changes/`.
+
+ssh
+~~~
+
+
+* The `--force-message` option is removed from the
+The link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/cmd-review.html[
+`review` command].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1908[Issue 1908]:
+Provide more informative error messages when rejecting updates.
+
+* Remove the limit in the query of patch sets by revision.
+
+* Add `isDraft` in the `patchSet` attribute of `stream-events` data.
++
+This allows consumers of the event stream to determine whether or not
+the event is related to a draft patch set.
+
+* Normalize the case of review labels submitted via the
+The link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/cmd-review.html[
+`review` command].
+
+* The `@CommandMetaData(descr)` annotation is deprecated in favor of `@CommandMetaData(description)`.
+
+
+Emails
+~~~~~~
+
+* Email notifications are sent for new changes created via actions in the
+Web UI such as cherry-picking or reverting a change.
+
+
+Tools
+~~~~~
+
+
+* git-exproll.sh: return non-zero on errors
+
+
+Documentation
+-------------
+
+
+* The link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/index.html[
+documentation index page] is rewritten in a hierarchical structure.
+
+* Various spelling mistakes are corrected in the documentation and previous
+release notes.
+
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 974d4db..c6c45eb 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,11 @@
Gerrit Code Review - Release Notes
==================================
+[[2_8]]
+Version 2.8.x
+-------------
+* link:ReleaseNotes-2.8.html[2.8]
+
[[2_7]]
Version 2.7.x
-------------
@@ -10,11 +15,14 @@
Version 2.6.x
-------------
* link:ReleaseNotes-2.6.2.html[2.6.2]
+* link:ReleaseNotes-2.6.1.html[2.6.1]
* link:ReleaseNotes-2.6.html[2.6]
[[2_5]]
Version 2.5.x
-------------
+* link:ReleaseNotes-2.5.6.html[2.5.6]
+* link:ReleaseNotes-2.5.5.html[2.5.5]
* link:ReleaseNotes-2.5.4.html[2.5.4]
* link:ReleaseNotes-2.5.3.html[2.5.3]
* link:ReleaseNotes-2.5.2.html[2.5.2]
@@ -24,6 +32,8 @@
[[2_4]]
Version 2.4.x
-------------
+* link:ReleaseNotes-2.4.4.html[2.4.4]
+* link:ReleaseNotes-2.4.3.html[2.4.3]
* link:ReleaseNotes-2.4.2.html[2.4.2]
* link:ReleaseNotes-2.4.1.html[2.4.1]
* link:ReleaseNotes-2.4.html[2.4]
@@ -46,6 +56,8 @@
[[2_1]]
Version 2.1.x
-------------
+* link:ReleaseNotes-2.1.10.html[2.1.10]
+* link:ReleaseNotes-2.1.9.html[2.1.9]
* link:ReleaseNotes-2.1.8.html[2.1.8]
* link:ReleaseNotes-2.1.7.2.html[2.1.7.2]
* link:ReleaseNotes-2.1.7.html[2.1.7]
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..23c7033
--- /dev/null
+++ b/VERSION
@@ -0,0 +1,5 @@
+# Maven style API version (e.g. '2.x-SNAPSHOT').
+# Used by :api_install and :api_deploy targets
+# when talking to the destination repository.
+#
+GERRIT_VERSION = '2.8-SNAPSHOT'
diff --git a/contrib/.pep8rc b/contrib/.pep8rc
new file mode 100644
index 0000000..568bcfb
--- /dev/null
+++ b/contrib/.pep8rc
@@ -0,0 +1,5 @@
+[pep8]
+max_line_length = 80
+show_pep8 = True
+show_source = True
+ignore = E111,E121
diff --git a/contrib/bash_completion b/contrib/bash_completion
new file mode 100644
index 0000000..4c4a01f
--- /dev/null
+++ b/contrib/bash_completion
@@ -0,0 +1,67 @@
+# The MIT License
+#
+# Copyright (C) 2013 Sony Mobile Communications. All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# #########################################################################
+# This bash script adds tab-completion to the gerrit.sh script.
+#
+# Testing it out without installing
+# =================================
+#
+# To test out the completion without "installing" this, just run this file
+# directly, like so:
+#
+# . ~/path/to/bash_completion
+#
+# Note: There's a dot ('.') at the beginning of that command.
+#
+# After you do that, tab completion will immediately be made available in your
+# current Bash shell. It will not, however, be available next time you log in.
+#
+# Installing
+# ==========
+#
+# To install the completion, point to this file from your .bash_profile, like so:
+#
+# . ~/path/to/bash_completion
+#
+# Do the same in your .bashrc if .bashrc doesn't invoke .bash_profile.
+#
+# The settings will take effect the next time you log in.
+#
+# Uninstalling
+# ============
+#
+# To uninstall, just remove the line from your .bash_profile and .bashrc.
+# #########################################################################
+
+_gerrit_sh()
+{
+ local cur prev opts
+ COMPREPLY=()
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
+ opts="check restart run start status stop supervise"
+
+ COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+}
+complete -F _gerrit_sh gerrit.sh
+
diff --git a/contrib/check-valid-commit.py b/contrib/check-valid-commit.py
index ca1785e..150b310 100755
--- a/contrib/check-valid-commit.py
+++ b/contrib/check-valid-commit.py
@@ -1,5 +1,8 @@
#!/usr/bin/env python
-import commands
+
+from __future__ import print_function
+
+import subprocess
import getopt
import sys
@@ -24,8 +27,8 @@
try:
opts, args = getopt.getopt(sys.argv[1:], '', \
['change=', 'project=', 'branch=', 'commit=', 'patchset='])
- except getopt.GetoptError, err:
- print 'Error: %s' % (err)
+ except getopt.GetoptError as err:
+ print('Error: %s' % (err))
usage()
sys.exit(-1)
@@ -41,7 +44,7 @@
elif arg == '--patchset':
patchset = value
else:
- print 'Error: option %s not recognized' % (arg)
+ print('Error: option %s not recognized' % (arg))
usage()
sys.exit(-1)
@@ -51,11 +54,11 @@
sys.exit(-1)
command = 'git cat-file commit %s' % (commit)
- status, output = commands.getstatusoutput(command)
+ status, output = subprocess.getstatusoutput(command)
if status != 0:
- print 'Error running \'%s\'. status: %s, output:\n\n%s' % \
- (command, status, output)
+ print('Error running \'%s\'. status: %s, output:\n\n%s' % \
+ (command, status, output))
sys.exit(-1)
commitMessage = output[(output.find('\n\n')+2):]
@@ -74,21 +77,21 @@
passes(commit)
def usage():
- print 'Usage:\n'
- print sys.argv[0] + ' --change <change id> --project <project name> ' \
- + '--branch <branch> --commit <sha1> --patchset <patchset id>'
+ print('Usage:\n')
+ print(sys.argv[0] + ' --change <change id> --project <project name> ' \
+ + '--branch <branch> --commit <sha1> --patchset <patchset id>')
def fail( commit, message ):
command = SSH_COMMAND + FAILURE_SCORE + ' -m \\\"' \
+ _shell_escape( FAILURE_MESSAGE + '\n\n' + message) \
+ '\\\" ' + commit
- commands.getstatusoutput(command)
+ subprocess.getstatusoutput(command)
sys.exit(1)
def passes( commit ):
command = SSH_COMMAND + PASS_SCORE + ' -m \\\"' \
+ _shell_escape(PASS_MESSAGE) + ' \\\" ' + commit
- commands.getstatusoutput(command)
+ subprocess.getstatusoutput(command)
def _shell_escape(x):
s = ''
diff --git a/contrib/git-exproll.sh b/contrib/git-exproll.sh
index 9526d9f..066c57c 100644
--- a/contrib/git-exproll.sh
+++ b/contrib/git-exproll.sh
@@ -126,7 +126,7 @@
[ -n "$1" ] && info "ERROR $1"
- exit
+ exit 128
}
debug() { [ -n "$SW_V" ] && info "$1" ; }
diff --git a/contrib/themes/diffy/etc/GerritSite.css b/contrib/themes/diffy/etc/GerritSite.css
index d476957..76c595a 100644
--- a/contrib/themes/diffy/etc/GerritSite.css
+++ b/contrib/themes/diffy/etc/GerritSite.css
@@ -15,15 +15,15 @@
#gerrit_topmenu {
left: 60px;
margin-right: 60px;
- padding-right: 10px;
position: relative;
+ margin-bottom: 5px;
}
#diffy_logo {
display: block !important;
margin-bottom: -55px;
- padding-left: 20px;
+ padding-left: 10px;
position: relative;
- top: -45px;
+ top: -55px;
width: 60px;
}
diff --git a/contrib/trivial_rebase.py b/contrib/trivial_rebase.py
index 30e60af..c97172e 100755
--- a/contrib/trivial_rebase.py
+++ b/contrib/trivial_rebase.py
@@ -36,6 +36,8 @@
"""
+from __future__ import print_function
+
import argparse
import json
import re
@@ -95,7 +97,7 @@
try:
process = subprocess.Popen(command, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
std_out, std_err = process.communicate()
- except OSError, e:
+ except OSError as e:
raise self.CheckCallError(command, cwd, e.errno, None)
if process.returncode:
raise self.CheckCallError(command, cwd, process.returncode, std_out, std_err)
@@ -107,9 +109,9 @@
'--format', 'JSON', '-c', sql_query]
try:
(gsql_out, _gsql_stderr) = self.CheckCall(gsql_cmd)
- except self.CheckCallError, e:
- print "return code is %s" % e.retcode
- print "stdout and stderr is\n%s%s" % (e.stdout, e.stderr)
+ except self.CheckCallError as e:
+ print("return code is %s" % e.retcode)
+ print("stdout and stderr is\n%s%s" % (e.stdout, e.stderr))
raise
new_out = gsql_out.replace('}}\n', '}}\nsplit here\n')
@@ -194,7 +196,7 @@
prev_patch_id = self.GetPatchId(prev_revision)
cur_patch_id = self.GetPatchId(self.commit)
if prev_patch_id == '0' and cur_patch_id == '0':
- print "commits %s and %s are both empty or merge commits" % (prev_revision, self.commit)
+ print("commits %s and %s are both empty or merge commits" % (prev_revision, self.commit))
return
if cur_patch_id != prev_patch_id:
# patch-ids don't match
@@ -238,7 +240,7 @@
gerrit_review_msg = ("\'Automatically re-added by Gerrit trivial rebase "
"detection script.\'")
- for acct, flags in self.acct_approvals.items():
+ for acct, flags in list(self.acct_approvals.items()):
gerrit_review_cmd = ['gerrit', 'review', '--project', self.project,
'--message', gerrit_review_msg, flags, self.commit]
email_addr = self.GetEmailFromAcctId(acct)
@@ -247,5 +249,5 @@
if __name__ == "__main__":
try:
TrivialRebase().Run()
- except AssertionError, e:
- print >> sys.stderr, e
+ except AssertionError as e:
+ print(e, file=sys.stderr)
diff --git a/gerrit-acceptance-tests/BUCK b/gerrit-acceptance-tests/BUCK
new file mode 100644
index 0000000..fc65f2c
--- /dev/null
+++ b/gerrit-acceptance-tests/BUCK
@@ -0,0 +1,39 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+java_library(
+ name = 'lib',
+ srcs = glob(['src/test/java/com/google/gerrit/acceptance/*.java']),
+ deps = [
+ '//gerrit-common:server',
+ '//gerrit-extension-api:api',
+ '//gerrit-launcher:launcher',
+ '//gerrit-httpd:httpd',
+ '//gerrit-pgm:pgm',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//gerrit-sshd:sshd',
+
+ '//lib:args4j',
+ '//lib:gson',
+ '//lib:guava',
+ '//lib:gwtorm',
+ '//lib:h2',
+ '//lib:jsch',
+ '//lib:jsr305',
+ '//lib:junit',
+ '//lib:servlet-api-3_0',
+
+ '//lib/commons:httpclient',
+ '//lib/commons:httpcore',
+ '//lib/log:impl_log4j',
+ '//lib/log:log4j',
+ '//lib/guice:guice',
+ '//lib/jgit:jgit',
+ '//lib/jgit:junit',
+ ],
+ export_deps = True,
+ visibility = [
+ '//tools/eclipse:classpath',
+ '//gerrit-acceptance-tests/...',
+ ],
+)
diff --git a/gerrit-acceptance-tests/pom.xml b/gerrit-acceptance-tests/pom.xml
deleted file mode 100644
index 63facba..0000000
--- a/gerrit-acceptance-tests/pom.xml
+++ /dev/null
@@ -1,148 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2013 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-acceptance-tests</artifactId>
-
- <name>Gerrit Code Review - Acceptance Tests</name>
-
- <description>
- Gerrit Acceptance Tests
- </description>
-
- <dependencies>
- <dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <scope>test</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-reviewdb</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-main</artifactId>
- <version>${project.version}</version>
- <scope>runtime</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-server</artifactId>
- <version>${project.version}</version>
- <exclusions>
- <exclusion>
- <groupId>org.apache.tomcat</groupId>
- <artifactId>servlet-api</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-log4j12</artifactId>
- </dependency>
-
- <dependency>
- <groupId>log4j</groupId>
- <artifactId>log4j</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-openid</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-sshd</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-httpd</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-pgm</artifactId>
- <version>${project.version}</version>
- <exclusions>
- <exclusion>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-servlet</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-servlet</artifactId>
- <scope>provided</scope>
- </dependency>
- </dependencies>
-
- <profiles>
- <profile>
- <id>acceptance</id>
- <activation>
- <property>
- <name>!gerrit.acceptance-tests.skip</name>
- </property>
- </activation>
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-failsafe-plugin</artifactId>
- <version>2.5</version>
- <executions>
- <execution>
- <id>integration-test</id>
- <goals>
- <goal>integration-test</goal>
- </goals>
- </execution>
- <execution>
- <id>verify</id>
- <goals>
- <goal>verify</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
- </profile>
- </profiles>
-</project>
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 51c7b3d..adb346f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -14,22 +14,51 @@
package com.google.gerrit.acceptance;
-import org.junit.After;
-import org.junit.Before;
-
+import org.eclipse.jgit.lib.Config;
+import org.junit.Rule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
public abstract class AbstractDaemonTest {
+ protected GerritServer server;
- private GerritServer server;
+ @Rule
+ public TestRule testRunner = new TestRule() {
+ @Override
+ public Statement apply(final Statement base, final Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ beforeTest(config(description));
+ base.evaluate();
+ afterTest();
+ }
+ };
+ }
+ };
- @Before
- public final void beforeTest() throws Exception {
- server = GerritServer.start();
+ private static Config config(Description description) {
+ GerritConfigs cfgs = description.getAnnotation(GerritConfigs.class);
+ GerritConfig cfg = description.getAnnotation(GerritConfig.class);
+ if (cfgs != null && cfg != null) {
+ throw new IllegalStateException("Use either @GerritConfigs or @GerritConfig not both");
+ }
+ if (cfgs != null) {
+ return ConfigAnnotationParser.parse(cfgs);
+ } else if (cfg != null) {
+ return ConfigAnnotationParser.parse(cfg);
+ } else {
+ return null;
+ }
+ }
+
+ private void beforeTest(Config cfg) throws Exception {
+ server = GerritServer.start(cfg);
server.getTestInjector().injectMembers(this);
}
- @After
- public final void afterTest() throws Exception {
+ private void afterTest() throws Exception {
server.stop();
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index f56ef07..cb785b9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -114,6 +114,12 @@
return create(username, null, username, (String[]) null);
}
+ public TestAccount admin()
+ throws UnsupportedEncodingException, OrmException, JSchException {
+ return create("admin", "admin@example.com", "Administrator",
+ "Administrators");
+ }
+
private AccountExternalId.Key getEmailKey(String email) {
return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
new file mode 100644
index 0000000..cf60fb4
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.ArrayList;
+
+class ConfigAnnotationParser {
+
+ private static Splitter splitter = Splitter.on(".").trimResults();
+
+ static Config parse(GerritConfigs annotation) {
+ if (annotation == null) {
+ return null;
+ }
+
+ Config cfg = new Config();
+ for (GerritConfig c : annotation.value()) {
+ parse(cfg, c);
+ }
+ return cfg;
+ }
+
+ static Config parse(GerritConfig annotation) {
+ Config cfg = new Config();
+ parse(cfg, annotation);
+ return cfg;
+ }
+
+ static private void parse(Config cfg, GerritConfig c) {
+ ArrayList<String> l = Lists.newArrayList(splitter.split(c.name()));
+ if (l.size() == 2) {
+ cfg.setString(l.get(0), null, l.get(1), c.value());
+ } else if (l.size() == 3) {
+ cfg.setString(l.get(0), l.get(1), l.get(2), c.value());
+ } else {
+ throw new IllegalArgumentException(
+ "GerritConfig.name must be of the format"
+ + " section.subsection.name or section.name");
+ }
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
new file mode 100644
index 0000000..5cb1229
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({METHOD})
+@Retention(RUNTIME)
+public @interface GerritConfig {
+ String name();
+ String value();
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
new file mode 100644
index 0000000..58bb9f2
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfigs.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({METHOD})
+@Retention(RUNTIME)
+public @interface GerritConfigs {
+ public GerritConfig[] value();
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 65bab6a..f97d2b9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -14,10 +14,28 @@
package com.google.gerrit.acceptance;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.pgm.Daemon;
+import com.google.gerrit.pgm.Init;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.util.SocketUtil;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.RepositoryCache;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.io.IOException;
import java.lang.reflect.Field;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.URI;
+import java.net.UnknownHostException;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
@@ -25,22 +43,12 @@
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
-import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.pgm.Daemon;
-import com.google.gerrit.pgm.Init;
-import com.google.gerrit.server.config.FactoryModule;
-import com.google.inject.Injector;
-import com.google.inject.Module;
-
-class GerritServer {
+public class GerritServer {
/** Returns fully started Gerrit server */
- static GerritServer start() throws Exception {
-
- final String sitePath = initSite();
-
+ static GerritServer start(Config base) throws Exception {
+ final File site = initSite(base);
final CyclicBarrier serverStarted = new CyclicBarrier(2);
-
final Daemon daemon = new Daemon(new Runnable() {
public void run() {
try {
@@ -56,10 +64,10 @@
ExecutorService daemonService = Executors.newSingleThreadExecutor();
daemonService.submit(new Callable<Void>() {
public Void call() throws Exception {
- int rc = daemon.main(new String[] {"-d", sitePath, "--headless" });
+ int rc = daemon.main(new String[] {"-d", site.getPath(), "--headless" });
if (rc != 0) {
System.out.println("Failed to start Gerrit daemon. Check "
- + sitePath + "/logs/error_log");
+ + site.getPath() + "/logs/error_log");
serverStarted.reset();
}
return null;
@@ -70,18 +78,40 @@
System.out.println("Gerrit Server Started");
Injector i = createTestInjector(daemon);
- return new GerritServer(i, daemon, daemonService);
+ return new GerritServer(site, i, daemon, daemonService);
}
- private static String initSite() throws Exception {
- DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
- String path = "target/test_site_" + df.format(new Date());
+ private static File initSite(Config base) throws Exception {
+ File tmp = TempFileUtil.createTempDirectory();
Init init = new Init();
- int rc = init.main(new String[] {"-d", path, "--batch", "--no-auto-start"});
+ int rc = init.main(new String[] {
+ "-d", tmp.getPath(), "--batch", "--no-auto-start",
+ "--skip-plugins"});
if (rc != 0) {
throw new RuntimeException("Couldn't initialize site");
}
- return path;
+
+ InetSocketAddress http = newPort();
+ InetSocketAddress sshd = newPort();
+ String url = "http://" + format(http) + "/";
+ MergeableFileBasedConfig cfg = new MergeableFileBasedConfig(
+ new File(new File(tmp, "etc"), "gerrit.config"),
+ FS.DETECTED);
+ cfg.load();
+ cfg.merge(base);
+ cfg.setString("gerrit", null, "canonicalWebUrl", url);
+ cfg.setString("httpd", null, "listenUrl", url);
+ cfg.setString("sshd", null, "listenAddress", format(sshd));
+ cfg.setString("cache", null, "directory", null);
+ cfg.setBoolean("sendemail", null, "enable", false);
+ cfg.setInt("cache", "projects", "checkFrequency", 0);
+ cfg.setInt("plugins", null, "checkFrequency", 0);
+ cfg.save();
+ return tmp;
+ }
+
+ private static String format(InetSocketAddress s) {
+ return String.format("%s:%d", s.getAddress().getHostAddress(), s.getPort());
}
private static Injector createTestInjector(Daemon daemon) throws Exception {
@@ -103,15 +133,66 @@
return (T) f.get(obj);
}
+ private static final InetSocketAddress newPort() throws IOException {
+ ServerSocket s = new ServerSocket(0, 0, getLocalHost());
+ try {
+ return (InetSocketAddress) s.getLocalSocketAddress();
+ } finally {
+ s.close();
+ }
+ }
+
+ private static InetAddress getLocalHost() throws UnknownHostException {
+ try {
+ return InetAddress.getLocalHost();
+ } catch (UnknownHostException e1) {
+ try {
+ return InetAddress.getByName("localhost");
+ } catch (UnknownHostException e2) {
+ return InetAddress.getByName("127.0.0.1");
+ }
+ }
+ }
+
+ private File sitePath;
private Daemon daemon;
private ExecutorService daemonService;
private Injector testInjector;
+ private String url;
+ private InetSocketAddress sshdAddress;
+ private InetSocketAddress httpAddress;
- private GerritServer(Injector testInjector,
- Daemon daemon, ExecutorService daemonService) {
+ private GerritServer(File sitePath, Injector testInjector, Daemon daemon,
+ ExecutorService daemonService) throws IOException, ConfigInvalidException {
+ this.sitePath = sitePath;
this.testInjector = testInjector;
this.daemon = daemon;
this.daemonService = daemonService;
+
+ FileBasedConfig cfg = new FileBasedConfig(
+ new File(new File(sitePath, "etc"), "gerrit.config"),
+ FS.DETECTED);
+ cfg.load();
+
+ url = cfg.getString("gerrit", null, "canonicalWebUrl");
+ URI uri = URI.create(url);
+
+ sshdAddress = SocketUtil.resolve(
+ cfg.getString("sshd", null, "listenAddress"),
+ 0);
+ httpAddress = new InetSocketAddress(uri.getHost(), uri.getPort());
+ }
+
+ String getUrl() {
+ return url;
+ }
+
+ InetSocketAddress getSshdAddress() {
+ return sshdAddress;
+ }
+
+ InetSocketAddress getHttpAddress() {
+ return httpAddress;
}
Injector getTestInjector() {
@@ -124,5 +205,7 @@
manager.stop();
daemonService.shutdownNow();
daemonService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+ TempFileUtil.recursivelyDelete(sitePath);
+ RepositoryCache.clear();
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java
new file mode 100644
index 0000000..f1baa9d
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/MergeableFileBasedConfig.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.common.collect.Lists;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+
+/**
+ * A file based Config that can merge another Config instance.
+ */
+public class MergeableFileBasedConfig extends FileBasedConfig {
+ public MergeableFileBasedConfig(File cfgLocation, FS fs) {
+ super(cfgLocation, fs);
+ }
+
+ /**
+ * Merge another Config into this Config.
+ *
+ * In case a configuration parameter exists both in this instance and in the
+ * merged instance then the value in this instance will simply replaced by
+ * the value from the merged instance.
+ *
+ * @param s Config to merge into this instance
+ */
+ public void merge(Config s) {
+ if (s == null) {
+ return;
+ }
+ for (String section : s.getSections()) {
+ for (String subsection : s.getSubsections(section)) {
+ for (String name : s.getNames(section, subsection)) {
+ setStringList(section, subsection, name, Lists.newArrayList(s
+ .getStringList(section, subsection, name)));
+ }
+ }
+
+ for (String name : s.getNames(section)) {
+ setStringList(section, null, name,
+ Lists.newArrayList(s.getStringList(section, null, name)));
+ }
+ }
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
index 2bf6523..9132be8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
@@ -14,6 +14,8 @@
package com.google.gerrit.acceptance;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Charsets;
import com.google.gson.Gson;
import org.apache.http.auth.AuthScope;
@@ -25,21 +27,23 @@
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;
-import org.apache.http.protocol.HTTP;
import java.io.IOException;
+import java.net.URI;
public class RestSession {
private final TestAccount account;
+ private final String url;
DefaultHttpClient client;
- public RestSession(TestAccount account) {
+ public RestSession(GerritServer server, TestAccount account) {
+ this.url = CharMatcher.is('/').trimTrailingFrom(server.getUrl());
this.account = account;
}
public RestResponse get(String endPoint) throws IOException {
- HttpGet get = new HttpGet("http://localhost:8080/a" + endPoint);
+ HttpGet get = new HttpGet(url + "/a" + endPoint);
return new RestResponse(getClient().execute(get));
}
@@ -48,10 +52,12 @@
}
public RestResponse put(String endPoint, Object content) throws IOException {
- HttpPut put = new HttpPut("http://localhost:8080/a" + endPoint);
+ HttpPut put = new HttpPut(url + "/a" + endPoint);
if (content != null) {
put.addHeader(new BasicHeader("Content-Type", "application/json"));
- put.setEntity(new StringEntity((new Gson()).toJson(content), HTTP.UTF_8));
+ put.setEntity(new StringEntity(
+ new Gson().toJson(content),
+ Charsets.UTF_8.name()));
}
return new RestResponse(getClient().execute(put));
}
@@ -61,24 +67,27 @@
}
public RestResponse post(String endPoint, Object content) throws IOException {
- HttpPost post = new HttpPost("http://localhost:8080/a" + endPoint);
+ HttpPost post = new HttpPost(url + "/a" + endPoint);
if (content != null) {
post.addHeader(new BasicHeader("Content-Type", "application/json"));
- post.setEntity(new StringEntity((new Gson()).toJson(content), HTTP.UTF_8));
+ post.setEntity(new StringEntity(
+ new Gson().toJson(content),
+ Charsets.UTF_8.name()));
}
return new RestResponse(getClient().execute(post));
}
public RestResponse delete(String endPoint) throws IOException {
- HttpDelete delete = new HttpDelete("http://localhost:8080/a" + endPoint);
+ HttpDelete delete = new HttpDelete(url + "/a" + endPoint);
return new RestResponse(getClient().execute(delete));
}
private DefaultHttpClient getClient() {
if (client == null) {
+ URI uri = URI.create(url);
client = new DefaultHttpClient();
client.getCredentialsProvider().setCredentials(
- new AuthScope("localhost", 8080),
+ new AuthScope(uri.getHost(), uri.getPort()),
new UsernamePasswordCredentials(account.username, account.httpPassword));
}
return client;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
index aae9236..a150eba 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
@@ -16,6 +16,7 @@
import java.io.IOException;
import java.io.InputStream;
+import java.net.InetSocketAddress;
import java.util.Scanner;
import com.jcraft.jsch.ChannelExec;
@@ -25,11 +26,13 @@
public class SshSession {
+ private final InetSocketAddress addr;
private final TestAccount account;
private Session session;
private String error;
- public SshSession(TestAccount account) {
+ public SshSession(GerritServer server, TestAccount account) {
+ this.addr = server.getSshdAddress();
this.account = account;
}
@@ -42,7 +45,7 @@
channel.connect();
Scanner s = new Scanner(channel.getErrStream()).useDelimiter("\\A");
- error = s.hasNext() ? s.next() : null;
+ error = s.hasNext() ? s.next() : null;
s = new Scanner(in).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
@@ -71,7 +74,10 @@
JSch jsch = new JSch();
jsch.addIdentity("KeyPair",
account.privateKey(), account.sshKey.getPublicKeyBlob(), null);
- session = jsch.getSession(account.username, "localhost", 29418);
+ session = jsch.getSession(
+ account.username,
+ addr.getAddress().getHostAddress(),
+ addr.getPort());
session.setConfig("StrictHostKeyChecking", "no");
session.connect();
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SystemGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SystemGroupsIT.java
deleted file mode 100644
index 6ee2045..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SystemGroupsIT.java
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance;
-
-import static org.junit.Assert.assertTrue;
-
-import com.google.common.collect.Sets;
-import com.google.gerrit.acceptance.rest.group.GroupInfo;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-
-import com.jcraft.jsch.JSchException;
-
-import org.junit.Before;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * An example test that tests presence of system groups in a newly initialized
- * review site.
- *
- * The test shows how to perform these checks via SSH, REST or using Gerrit
- * internals.
- */
-public class SystemGroupsIT extends AbstractDaemonTest {
-
- @Inject
- private SchemaFactory<ReviewDb> reviewDbProvider;
-
- @Inject
- private AccountCreator accounts;
-
- protected TestAccount admin;
-
- @Before
- public void setUp() throws Exception {
- admin = accounts.create("admin", "admin@sap.com", "Administrator",
- "Administrators");
- }
-
- @Test
- public void systemGroupsCreated_ssh() throws JSchException, IOException {
- SshSession session = new SshSession(admin);
- String result = session.exec("gerrit ls-groups");
- assertTrue(result.contains("Administrators"));
- assertTrue(result.contains("Anonymous Users"));
- assertTrue(result.contains("Non-Interactive Users"));
- assertTrue(result.contains("Project Owners"));
- assertTrue(result.contains("Registered Users"));
- session.close();
- }
-
- @Test
- public void systemGroupsCreated_rest() throws IOException {
- RestSession session = new RestSession(admin);
- RestResponse r = session.get("/groups/");
- Gson gson = new Gson();
- Map<String, GroupInfo> result =
- gson.fromJson(r.getReader(), new TypeToken<Map<String, GroupInfo>>() {}.getType());
- Set<String> names = result.keySet();
- assertTrue(names.contains("Administrators"));
- assertTrue(names.contains("Anonymous Users"));
- assertTrue(names.contains("Non-Interactive Users"));
- assertTrue(names.contains("Project Owners"));
- assertTrue(names.contains("Registered Users"));
- }
-
- @Test
- public void systemGroupsCreated_internals() throws OrmException {
- ReviewDb db = reviewDbProvider.open();
- try {
- Set<String> names = Sets.newHashSet();
- for (AccountGroup g : db.accountGroups().all()) {
- names.add(g.getName());
- }
- assertTrue(names.contains("Administrators"));
- assertTrue(names.contains("Anonymous Users"));
- assertTrue(names.contains("Non-Interactive Users"));
- assertTrue(names.contains("Project Owners"));
- assertTrue(names.contains("Registered Users"));
- } finally {
- db.close();
- }
- }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java
index adee361..ff0ca7b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java
@@ -15,27 +15,41 @@
package com.google.gerrit.acceptance;
import java.io.File;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
+import java.io.IOException;
public class TempFileUtil {
-
- private static int testCount;
- private static DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
- private static final File temp = new File(new File("target"), "temp");
-
- private static String createUniqueTestFolderName() {
- return "test_" + (df.format(new Date()) + "_" + (testCount++));
+ public static File createTempDirectory() throws IOException {
+ File tmp = File.createTempFile("gerrit_test_", "");
+ if (!tmp.delete() || !tmp.mkdir()) {
+ throw new IOException("Cannot create " + tmp.getPath());
+ }
+ return tmp;
}
- public static File createTempDirectory() {
- final String name = createUniqueTestFolderName();
- final File directory = new File(temp, name);
- if (!directory.mkdirs()) {
- throw new RuntimeException("failed to create folder '"
- + directory.getAbsolutePath() + "'");
+ public static void recursivelyDelete(File dir) throws IOException {
+ if (!dir.getPath().equals(dir.getCanonicalPath())) {
+ // Directory symlink reaching outside of temporary space.
+ return;
}
- return directory;
+ File[] contents = dir.listFiles();
+ if (contents != null) {
+ for (File d : contents) {
+ if (d.isDirectory()) {
+ recursivelyDelete(d);
+ } else {
+ deleteNowOrOnExit(d);
+ }
+ }
+ }
+ deleteNowOrOnExit(dir);
+ }
+
+ private static void deleteNowOrOnExit(File dir) {
+ if (!dir.delete()) {
+ dir.deleteOnExit();
+ }
+ }
+
+ private TempFileUtil() {
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
index 358680f..b85ffea 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
@@ -51,13 +51,11 @@
return new PersonIdent(username, email);
}
- public String getHttpUrl() {
- StringBuilder b = new StringBuilder();
- b.append("http://");
- b.append(username);
- b.append(":");
- b.append(httpPassword);
- b.append("@localhost:8080");
- return b.toString();
+ public String getHttpUrl(GerritServer server) {
+ return String.format("http://%s:%s@%s:%d",
+ username,
+ httpPassword,
+ server.getHttpAddress().getAddress().getHostAddress(),
+ server.getHttpAddress().getPort());
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
new file mode 100644
index 0000000..0931e12
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/UseGerritConfigAnnotationTest.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class UseGerritConfigAnnotationTest extends AbstractDaemonTest {
+
+ @Inject
+ @GerritServerConfig
+ Config serverConfig;
+
+ @Test
+ @GerritConfig(name="x.y", value="z")
+ public void testOne() {
+ assertEquals("z", serverConfig.getString("x", null, "y"));
+ }
+
+ @Test
+ @GerritConfigs({
+ @GerritConfig(name="x.y", value="z"),
+ @GerritConfig(name="a.b", value="c"),
+ })
+ public void testMultiple() {
+ assertEquals("z", serverConfig.getString("x", null, "y"));
+ assertEquals("c", serverConfig.getString("a", null, "b"));
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
new file mode 100644
index 0000000..a6ce132
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -0,0 +1,196 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public abstract class AbstractPushForReview extends AbstractDaemonTest {
+ protected enum Protocol {
+ SSH, HTTP
+ }
+
+ @Inject
+ private AccountCreator accounts;
+
+ @Inject
+ private SchemaFactory<ReviewDb> reviewDbProvider;
+
+ private TestAccount admin;
+ private Project.NameKey project;
+ private Git git;
+ private ReviewDb db;
+ private String sshUrl;
+
+ @Before
+ public void setUp() throws Exception {
+ admin =
+ accounts.create("admin", "admin@example.com", "Administrator",
+ "Administrators");
+
+ project = new Project.NameKey("p");
+ initSsh(admin);
+ SshSession sshSession = new SshSession(server, admin);
+ createProject(sshSession, project.get());
+ sshUrl = sshSession.getUrl();
+ sshSession.close();
+
+ db = reviewDbProvider.open();
+ }
+
+ protected void selectProtocol(Protocol p) throws GitAPIException, IOException {
+ String url;
+ switch (p) {
+ case SSH:
+ url = sshUrl;
+ break;
+ case HTTP:
+ url = admin.getHttpUrl(server);
+ break;
+ default:
+ throw new IllegalArgumentException("unexpected protocol: " + p);
+ }
+ git = cloneProject(url + "/" + project.get());
+ }
+
+ @After
+ public void cleanup() {
+ db.close();
+ }
+
+ @Test
+ public void testPushForMaster() throws GitAPIException, OrmException,
+ IOException {
+ PushOneCommit.Result r = pushTo("refs/for/master");
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, null);
+ }
+
+ @Test
+ public void testPushForMasterWithTopic() throws GitAPIException,
+ OrmException, IOException {
+ // specify topic in ref
+ String topic = "my/topic";
+ PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, topic);
+
+ // specify topic as option
+ r = pushTo("refs/for/master%topic=" + topic);
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, topic);
+ }
+
+ @Test
+ public void testPushForMasterWithCc() throws GitAPIException, OrmException,
+ IOException, JSchException {
+ // cc one user
+ TestAccount user = accounts.create("user", "user@example.com", "User");
+ String topic = "my/topic";
+ PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, topic);
+
+ // cc several users
+ TestAccount user2 =
+ accounts.create("another-user", "another.user@example.com", "Another User");
+ r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
+ + user.email + ",cc=" + user2.email);
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, topic);
+
+ // cc non-existing user
+ String nonExistingEmail = "non.existing@example.com";
+ r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
+ + nonExistingEmail + ",cc=" + user.email);
+ r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
+ }
+
+ @Test
+ public void testPushForMasterWithReviewer() throws GitAPIException,
+ OrmException, IOException, JSchException {
+ // add one reviewer
+ TestAccount user = accounts.create("user", "user@example.com", "User");
+ String topic = "my/topic";
+ PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email);
+ r.assertOkStatus();
+ r.assertChange(Change.Status.NEW, topic, user);
+
+ // add several reviewers
+ TestAccount user2 =
+ accounts.create("another-user", "another.user@example.com", "Another User");
+ r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r=" + user.email
+ + ",r=" + user2.email);
+ r.assertOkStatus();
+ // admin is the owner of the change and should not appear as reviewer
+ r.assertChange(Change.Status.NEW, topic, user, user2);
+
+ // add non-existing user as reviewer
+ String nonExistingEmail = "non.existing@example.com";
+ r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r="
+ + nonExistingEmail + ",r=" + user.email);
+ r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
+ }
+
+ @Test
+ public void testPushForMasterAsDraft() throws GitAPIException, OrmException,
+ IOException {
+ // create draft by pushing to 'refs/drafts/'
+ PushOneCommit.Result r = pushTo("refs/drafts/master");
+ r.assertOkStatus();
+ r.assertChange(Change.Status.DRAFT, null);
+
+ // create draft by using 'draft' option
+ r = pushTo("refs/for/master%draft");
+ r.assertOkStatus();
+ r.assertChange(Change.Status.DRAFT, null);
+ }
+
+ @Test
+ public void testPushForNonExistingBranch() throws GitAPIException,
+ OrmException, IOException {
+ String branchName = "non-existing";
+ PushOneCommit.Result r = pushTo("refs/for/" + branchName);
+ r.assertErrorStatus("branch " + branchName + " not found");
+ }
+
+ private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
+ IOException {
+ PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+ return push.to(git, ref);
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
new file mode 100644
index 0000000..6014118
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/BUCK
@@ -0,0 +1,38 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+ srcs = ['SubmitOnPushIT.java'],
+ deps = [':util'],
+)
+
+acceptance_tests(
+ srcs = ['HttpPushForReviewIT.java', 'SshPushForReviewIT.java'],
+ deps = [':push_for_review'],
+)
+
+java_library(
+ name = 'push_for_review',
+ srcs = ['AbstractPushForReview.java'],
+ deps = [
+ ':util',
+ '//gerrit-acceptance-tests:lib',
+ ],
+)
+
+java_library(
+ name = 'util',
+ srcs = [
+ 'GitUtil.java',
+ 'PushOneCommit.java',
+ ],
+ deps = [
+ '//gerrit-acceptance-tests:lib',
+ '//gerrit-reviewdb:server',
+ '//lib:guava',
+ '//lib:gwtorm',
+ '//lib:jsch',
+ '//lib/jgit:jgit',
+ '//lib:junit',
+ ],
+ visibility = ['//gerrit-acceptance-tests/...'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
index 045207c..5f8faff 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
@@ -18,6 +18,7 @@
import com.google.gerrit.acceptance.SshSession;
import com.google.gerrit.acceptance.TempFileUtil;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Project;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
@@ -74,10 +75,34 @@
public static void createProject(SshSession s, String name)
throws JSchException, IOException {
- s.exec("gerrit create-project --empty-commit --name \"" + name + "\"");
+ createProject(s, name, null);
}
- public static Git cloneProject(String url) throws GitAPIException {
+ public static void createProject(SshSession s, String name, Project.NameKey parent)
+ throws JSchException, IOException {
+ createProject(s, name, parent, true);
+ }
+
+ public static void createProject(SshSession s, String name,
+ Project.NameKey parent, boolean emptyCommit)
+ throws JSchException, IOException {
+ StringBuilder b = new StringBuilder();
+ b.append("gerrit create-project");
+ if (emptyCommit) {
+ b.append(" --empty-commit");
+ }
+ b.append(" --name \"");
+ b.append(name);
+ b.append("\"");
+ if (parent != null) {
+ b.append(" --parent \"");
+ b.append(parent.get());
+ b.append("\"");
+ }
+ s.exec(b.toString());
+ }
+
+ public static Git cloneProject(String url) throws GitAPIException, IOException {
final File gitDir = TempFileUtil.createTempDirectory();
final CloneCommand cloneCmd = Git.cloneRepository();
cloneCmd.setURI(url);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
new file mode 100644
index 0000000..465befd
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/HttpPushForReviewIT.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.Before;
+
+import java.io.IOException;
+
+public class HttpPushForReviewIT extends AbstractPushForReview {
+ @Before
+ public void selectHttpUrl() throws GitAPIException, IOException {
+ selectProtocol(Protocol.HTTP);
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java
deleted file mode 100644
index 9799cc1..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java
+++ /dev/null
@@ -1,268 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.git;
-
-import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.git.GitUtil.createProject;
-import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AccountCreator;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-
-import com.jcraft.jsch.JSchException;
-
-import org.eclipse.jgit.api.Git;
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.io.IOException;
-
-public class PushForReviewIT extends AbstractDaemonTest {
- private enum Protocol {
- SSH, HTTP
- }
-
- @Inject
- private AccountCreator accounts;
-
- @Inject
- private SchemaFactory<ReviewDb> reviewDbProvider;
-
- private TestAccount admin;
- private Project.NameKey project;
- private Git git;
- private ReviewDb db;
- private String sshUrl;
-
- @Before
- public void setUp() throws Exception {
- admin =
- accounts.create("admin", "admin@example.com", "Administrator",
- "Administrators");
-
- project = new Project.NameKey("p");
- initSsh(admin);
- SshSession sshSession = new SshSession(admin);
- createProject(sshSession, project.get());
- sshUrl = sshSession.getUrl();
- sshSession.close();
-
- db = reviewDbProvider.open();
- }
-
- private void selectProtocol(Protocol p) throws GitAPIException, IOException {
- String url;
- switch (p) {
- case SSH:
- url = sshUrl;
- break;
- case HTTP:
- url = admin.getHttpUrl();
- break;
- default:
- throw new IllegalArgumentException("unexpected protocol: " + p);
- }
- git = cloneProject(url + "/" + project.get());
- }
-
- @After
- public void cleanup() {
- db.close();
- }
-
- @Test
- public void testPushForMaster_HTTP() throws GitAPIException, OrmException,
- IOException {
- testPushForMaster(Protocol.HTTP);
- }
-
- @Test
- public void testPushForMaster_SSH() throws GitAPIException, OrmException,
- IOException {
- testPushForMaster(Protocol.SSH);
- }
-
- private void testPushForMaster(Protocol p) throws GitAPIException,
- OrmException, IOException {
- selectProtocol(p);
- PushOneCommit.Result r = pushTo("refs/for/master");
- r.assertOkStatus();
- r.assertChange(Change.Status.NEW, null);
- }
-
- @Test
- public void testPushForMasterWithTopic_HTTP()
- throws GitAPIException, OrmException, IOException {
- testPushForMasterWithTopic(Protocol.HTTP);
- }
-
- @Test
- public void testPushForMasterWithTopic_SSH()
- throws GitAPIException, OrmException, IOException {
- testPushForMasterWithTopic(Protocol.SSH);
- }
-
- private void testPushForMasterWithTopic(Protocol p) throws GitAPIException,
- OrmException, IOException {
- selectProtocol(p);
- // specify topic in ref
- String topic = "my/topic";
- PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
- r.assertOkStatus();
- r.assertChange(Change.Status.NEW, topic);
-
- // specify topic as option
- r = pushTo("refs/for/master%topic=" + topic);
- r.assertOkStatus();
- r.assertChange(Change.Status.NEW, topic);
- }
-
- @Test
- public void testPushForMasterWithCc_HTTP() throws GitAPIException,
- OrmException, IOException, JSchException {
- testPushForMasterWithCc(Protocol.HTTP);
- }
-
- @Test
- public void testPushForMasterWithCc_SSH() throws GitAPIException,
- OrmException, IOException, JSchException {
- testPushForMasterWithCc(Protocol.SSH);
- }
-
- private void testPushForMasterWithCc(Protocol p) throws GitAPIException,
- OrmException, IOException, JSchException {
- selectProtocol(p);
- // cc one user
- TestAccount user = accounts.create("user", "user@example.com", "User");
- String topic = "my/topic";
- PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
- r.assertOkStatus();
- r.assertChange(Change.Status.NEW, topic);
-
- // cc several users
- TestAccount user2 =
- accounts.create("another-user", "another.user@example.com", "Another User");
- r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
- + user.email + ",cc=" + user2.email);
- r.assertOkStatus();
- r.assertChange(Change.Status.NEW, topic);
-
- // cc non-existing user
- String nonExistingEmail = "non.existing@example.com";
- r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
- + nonExistingEmail + ",cc=" + user.email);
- r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
- }
-
- @Test
- public void testPushForMasterWithReviewer_HTTP() throws GitAPIException,
- OrmException, IOException, JSchException {
- testPushForMasterWithReviewer(Protocol.HTTP);
- }
-
- @Test
- public void testPushForMasterWithReviewer_SSH() throws GitAPIException,
- OrmException, IOException, JSchException {
- testPushForMasterWithReviewer(Protocol.SSH);
- }
-
- private void testPushForMasterWithReviewer(Protocol p)
- throws GitAPIException, OrmException, IOException, JSchException {
- selectProtocol(p);
- // add one reviewer
- TestAccount user = accounts.create("user", "user@example.com", "User");
- String topic = "my/topic";
- PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email);
- r.assertOkStatus();
- r.assertChange(Change.Status.NEW, topic, user);
-
- // add several reviewers
- TestAccount user2 =
- accounts.create("another-user", "another.user@example.com", "Another User");
- r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r=" + user.email
- + ",r=" + user2.email);
- r.assertOkStatus();
- // admin is the owner of the change and should not appear as reviewer
- r.assertChange(Change.Status.NEW, topic, user, user2);
-
- // add non-existing user as reviewer
- String nonExistingEmail = "non.existing@example.com";
- r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r="
- + nonExistingEmail + ",r=" + user.email);
- r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
- }
-
- @Test
- public void testPushForMasterAsDraft_HTTP() throws GitAPIException,
- OrmException, IOException {
- testPushForMasterAsDraft(Protocol.HTTP);
- }
-
- @Test
- public void testPushForMasterAsDraft_SSH() throws GitAPIException,
- OrmException, IOException {
- testPushForMasterAsDraft(Protocol.SSH);
- }
-
- private void testPushForMasterAsDraft(Protocol p) throws GitAPIException,
- OrmException, IOException {
- selectProtocol(p);
- // create draft by pushing to 'refs/drafts/'
- PushOneCommit.Result r = pushTo("refs/drafts/master");
- r.assertOkStatus();
- r.assertChange(Change.Status.DRAFT, null);
-
- // create draft by using 'draft' option
- r = pushTo("refs/for/master%draft");
- r.assertOkStatus();
- r.assertChange(Change.Status.DRAFT, null);
- }
-
- @Test
- public void testPushForNonExistingBranch_HTTP() throws GitAPIException,
- OrmException, IOException {
- testPushForNonExistingBranch(Protocol.HTTP);
- }
-
- @Test
- public void testPushForNonExistingBranch_SSH() throws GitAPIException,
- OrmException, IOException {
- testPushForNonExistingBranch(Protocol.SSH);
- }
-
- private void testPushForNonExistingBranch(Protocol p) throws GitAPIException,
- OrmException, IOException {
- selectProtocol(p);
- String branchName = "non-existing";
- PushOneCommit.Result r = pushTo("refs/for/" + branchName);
- r.assertErrorStatus("branch " + branchName + " not found");
- }
-
- private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
- IOException {
- PushOneCommit push = new PushOneCommit(db, admin.getIdent());
- return push.to(git, ref);
- }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
index 4c32f2f..7450565 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
@@ -46,10 +46,10 @@
import java.util.Set;
public class PushOneCommit {
- public final static String SUBJECT = "test commit";
+ public static final String SUBJECT = "test commit";
- private final static String FILE_NAME = "a.txt";
- private final static String FILE_CONTENT = "some content";
+ private static final String FILE_NAME = "a.txt";
+ private static final String FILE_CONTENT = "some content";
private final ReviewDb db;
private final PersonIdent i;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java
new file mode 100644
index 0000000..5251d2d
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.Before;
+
+import java.io.IOException;
+
+public class SshPushForReviewIT extends AbstractPushForReview {
+ @Before
+ public void selectSshUrl() throws GitAPIException, IOException {
+ selectProtocol(Protocol.SSH);
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index ab97d19..baeafe1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -97,7 +97,7 @@
project = new Project.NameKey("p");
initSsh(admin);
- SshSession sshSession = new SshSession(admin);
+ SshSession sshSession = new SshSession(server, admin);
createProject(sshSession, project.get());
git = cloneProject(sshSession.getUrl() + "/" + project.get());
sshSession.close();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
new file mode 100644
index 0000000..d813501
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/BUCK
@@ -0,0 +1,18 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+ srcs = glob(['*IT.java']),
+ deps = [':util'],
+)
+
+java_library(
+ name = 'util',
+ srcs = ['AccountAssert.java', 'AccountInfo.java'],
+ deps = [
+ '//gerrit-acceptance-tests:lib',
+ '//gerrit-reviewdb:server',
+ '//lib:gwtorm',
+ '//lib:junit',
+ ],
+ visibility = ['//gerrit-acceptance-tests/...'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
index 8d75487..3e8183e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
@@ -45,7 +45,7 @@
public void setUp() throws Exception {
admin = accounts.create("admin", "admin@example.com", "Administrator",
"Administrators");
- session = new RestSession(admin);
+ session = new RestSession(server, admin);
}
@Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
new file mode 100644
index 0000000..dff94ce
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
@@ -0,0 +1,14 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+ srcs = glob(['*IT.java']),
+ deps = [
+ ':util',
+ '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:util',
+ ],
+)
+
+java_library(
+ name = 'util',
+ srcs = ['ChangeInfo.java', 'ChangeMessageInfo.java'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeInfo.java
new file mode 100644
index 0000000..4c9325e
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeInfo.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import java.util.List;
+
+public class ChangeInfo {
+ List<ChangeMessageInfo> messages;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessageInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessageInfo.java
new file mode 100644
index 0000000..b3c584a
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessageInfo.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+public class ChangeMessageInfo {
+ String message;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
new file mode 100644
index 0000000..0573b1b
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -0,0 +1,151 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class ChangeMessagesIT extends AbstractDaemonTest {
+
+ @Inject
+ private AccountCreator accounts;
+
+ @Inject
+ private SchemaFactory<ReviewDb> reviewDbProvider;
+
+ private TestAccount admin;
+ private RestSession session;
+ private Git git;
+ private ReviewDb db;
+
+ @Before
+ public void setUp() throws Exception {
+ admin = accounts.create("admin", "admin@example.com", "Administrator",
+ "Administrators");
+ session = new RestSession(server, admin);
+ initSsh(admin);
+ Project.NameKey project = new Project.NameKey("p");
+ SshSession sshSession = new SshSession(server, admin);
+ createProject(sshSession, project.get());
+ git = cloneProject(sshSession.getUrl() + "/" + project.get());
+ sshSession.close();
+ db = reviewDbProvider.open();
+ }
+
+ @After
+ public void cleanup() {
+ db.close();
+ }
+
+ @Test
+ public void messagesNotReturnedByDefault() throws GitAPIException,
+ IOException {
+ String changeId = createChange();
+ postMessage(changeId, "Some nits need to be fixed.");
+ ChangeInfo c = getChange(changeId);
+ assertNull(c.messages);
+ }
+
+ @Test
+ public void noMessages() throws GitAPIException,
+ IOException {
+ String changeId = createChange();
+ ChangeInfo c = getChangeWithMessages(changeId);
+ assertNotNull(c.messages);
+ assertTrue(c.messages.isEmpty());
+ }
+
+ @Test
+ public void messagesReturnedInChronologicalOrder() throws GitAPIException,
+ IOException {
+ String changeId = createChange();
+ String firstMessage = "Some nits need to be fixed.";
+ postMessage(changeId, firstMessage);
+ String secondMessage = "I like this feature.";
+ postMessage(changeId, secondMessage);
+ ChangeInfo c = getChangeWithMessages(changeId);
+ assertNotNull(c.messages);
+ assertEquals(2, c.messages.size());
+ assertMessage(firstMessage, c.messages.get(0).message);
+ assertMessage(secondMessage, c.messages.get(1).message);
+ }
+
+ private String createChange() throws GitAPIException,
+ IOException {
+ PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+ return push.to(git, "refs/for/master").getChangeId();
+ }
+
+ private ChangeInfo getChange(String changeId) throws IOException {
+ return getChange(changeId, false);
+ }
+
+ private ChangeInfo getChangeWithMessages(String changeId) throws IOException {
+ return getChange(changeId, true);
+ }
+
+ private ChangeInfo getChange(String changeId, boolean includeMessages)
+ throws IOException {
+ RestResponse r =
+ session.get("/changes/?q=" + changeId
+ + (includeMessages ? "&o=MESSAGES" : ""));
+ List<ChangeInfo> c = (new Gson()).fromJson(r.getReader(),
+ new TypeToken<List<ChangeInfo>>() {}.getType());
+ return c.get(0);
+ }
+
+ private void assertMessage(String expected, String actual) {
+ assertEquals("Patch Set 1:\n\n" + expected, actual);
+ }
+
+ private void postMessage(String changeId, String msg) throws IOException {
+ ReviewInput in = new ReviewInput();
+ in.message = msg;
+ session.post("/changes/" + changeId + "/revisions/1/review", in).consume();
+ }
+
+ @SuppressWarnings("unused")
+ private class ReviewInput {
+ String message;
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
index a254f15..08211d0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
@@ -32,7 +32,7 @@
import com.google.gerrit.acceptance.rest.account.AccountInfo;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.GroupCache;
@@ -72,7 +72,7 @@
@Before
public void setUp() throws Exception {
admin = accounts.create("admin", "Administrators");
- session = new RestSession(admin);
+ session = new RestSession(server, admin);
db = reviewDbProvider.open();
}
@@ -223,9 +223,9 @@
throws OrmException {
AccountGroup g = groupCache.get(new AccountGroup.NameKey(group));
Set<AccountGroup.UUID> ids = Sets.newHashSet();
- ResultSet<AccountGroupIncludeByUuid> all =
- db.accountGroupIncludesByUuid().byGroup(g.getId());
- for (AccountGroupIncludeByUuid m : all) {
+ ResultSet<AccountGroupById> all =
+ db.accountGroupById().byGroup(g.getId());
+ for (AccountGroupById m : all) {
ids.add(m.getIncludeUUID());
}
assertTrue(ids.size() == includes.length);
@@ -252,8 +252,8 @@
private void assertNoIncludes(String group) throws OrmException {
AccountGroup g = groupCache.get(new AccountGroup.NameKey(group));
- Iterator<AccountGroupIncludeByUuid> it =
- db.accountGroupIncludesByUuid().byGroup(g.getId()).iterator();
+ Iterator<AccountGroupById> it =
+ db.accountGroupById().byGroup(g.getId()).iterator();
assertFalse(it.hasNext());
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
new file mode 100644
index 0000000..b6e017d
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/BUCK
@@ -0,0 +1,27 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+ srcs = glob(['*IT.java']),
+ deps = [
+ ':util',
+ '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account:util',
+ ],
+)
+
+java_library(
+ name = 'util',
+ srcs = [
+ 'GroupAssert.java',
+ 'GroupInfo.java',
+ 'GroupInput.java',
+ 'GroupOptionsInfo.java',
+ 'GroupsInput.java',
+ 'MembersInput.java',
+ ],
+ deps = [
+ '//gerrit-extension-api:api',
+ '//gerrit-reviewdb:server',
+ '//lib:gwtorm',
+ '//lib:junit',
+ ],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
index 4641f09..b7ab0fc 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
@@ -54,7 +54,7 @@
public void setUp() throws Exception {
admin = accounts.create("admin", "admin@example.com", "Administrator",
"Administrators");
- session = new RestSession(admin);
+ session = new RestSession(server, admin);
}
@Test
@@ -88,7 +88,7 @@
public void testCreateGroupWithoutCapability_Forbidden() throws OrmException,
JSchException, IOException {
TestAccount user = accounts.create("user", "user@example.com", "User");
- RestResponse r = (new RestSession(user)).put("/groups/newGroup");
+ RestResponse r = (new RestSession(server, user)).put("/groups/newGroup");
assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
index 5fe386d..67a7197 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
@@ -47,7 +47,7 @@
public void setUp() throws Exception {
admin = accounts.create("admin", "admin@example.com", "Administrator",
"Administrators");
- session = new RestSession(admin);
+ session = new RestSession(server, admin);
}
@Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
index ab22367..05d74a3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
@@ -53,7 +53,7 @@
public void setUp() throws Exception {
admin = accounts.create("admin", "admin@example.com", "Administrator",
"Administrators");
- session = new RestSession(admin);
+ session = new RestSession(server, admin);
}
@Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java
index 997f476..7b614e8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java
@@ -48,7 +48,7 @@
@Before
public void setUp() throws Exception {
TestAccount admin = accounts.create("admin", "Administrators");
- session = new RestSession(admin);
+ session = new RestSession(server, admin);
}
@Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java
index a5aa3dd..8dbd7f1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java
@@ -49,7 +49,7 @@
@Before
public void setUp() throws Exception {
TestAccount admin = accounts.create("admin", "Administrators");
- session = new RestSession(admin);
+ session = new RestSession(server, admin);
}
@Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
index 07c7c93..466041e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
@@ -58,7 +58,7 @@
public void setUp() throws Exception {
admin = accounts.create("admin", "admin@example.com", "Administrator",
"Administrators");
- session = new RestSession(admin);
+ session = new RestSession(server, admin);
}
@Test
@@ -84,7 +84,7 @@
expectedGroups.add("Anonymous Users");
expectedGroups.add("Registered Users");
TestAccount user = accounts.create("user", "user@example.com", "User");
- RestResponse r = (new RestSession(user)).get("/groups/");
+ RestResponse r = new RestSession(server, user).get("/groups/");
Map<String, GroupInfo> result =
(new Gson()).fromJson(r.getReader(), new TypeToken<Map<String, GroupInfo>>() {}.getType());
assertGroups(expectedGroups, result.keySet());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/SystemGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/SystemGroupsIT.java
new file mode 100644
index 0000000..764b7e8
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/SystemGroupsIT.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An example test that tests presence of system groups in a newly initialized
+ * review site.
+ *
+ * The test shows how to perform these checks via SSH, REST or using Gerrit
+ * internals.
+ */
+public class SystemGroupsIT extends AbstractDaemonTest {
+
+ @Inject
+ private SchemaFactory<ReviewDb> reviewDbProvider;
+
+ @Inject
+ private AccountCreator accounts;
+
+ protected TestAccount admin;
+
+ @Before
+ public void setUp() throws Exception {
+ admin = accounts.create("admin", "admin@sap.com", "Administrator",
+ "Administrators");
+ }
+
+ @Test
+ public void systemGroupsCreated_ssh() throws JSchException, IOException {
+ SshSession session = new SshSession(server, admin);
+ String result = session.exec("gerrit ls-groups");
+ assertTrue(result.contains("Administrators"));
+ assertTrue(result.contains("Anonymous Users"));
+ assertTrue(result.contains("Non-Interactive Users"));
+ assertTrue(result.contains("Project Owners"));
+ assertTrue(result.contains("Registered Users"));
+ session.close();
+ }
+
+ @Test
+ public void systemGroupsCreated_rest() throws IOException {
+ RestSession session = new RestSession(server, admin);
+ RestResponse r = session.get("/groups/");
+ Gson gson = new Gson();
+ Map<String, GroupInfo> result =
+ gson.fromJson(r.getReader(), new TypeToken<Map<String, GroupInfo>>() {}.getType());
+ Set<String> names = result.keySet();
+ assertTrue(names.contains("Administrators"));
+ assertTrue(names.contains("Anonymous Users"));
+ assertTrue(names.contains("Non-Interactive Users"));
+ assertTrue(names.contains("Project Owners"));
+ assertTrue(names.contains("Registered Users"));
+ }
+
+ @Test
+ public void systemGroupsCreated_internals() throws OrmException {
+ ReviewDb db = reviewDbProvider.open();
+ try {
+ Set<String> names = Sets.newHashSet();
+ for (AccountGroup g : db.accountGroups().all()) {
+ names.add(g.getName());
+ }
+ assertTrue(names.contains("Administrators"));
+ assertTrue(names.contains("Anonymous Users"));
+ assertTrue(names.contains("Non-Interactive Users"));
+ assertTrue(names.contains("Project Owners"));
+ assertTrue(names.contains("Registered Users"));
+ } finally {
+ db.close();
+ }
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
new file mode 100644
index 0000000..bb3bb30
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUCK
@@ -0,0 +1,38 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+ srcs = glob(['*IT.java']),
+ deps = [
+ ':branch',
+ ':project',
+ '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:util',
+ ],
+)
+
+java_library(
+ name = 'branch',
+ srcs = [
+ 'BranchAssert.java',
+ 'BranchInfo.java',
+ ],
+ deps = [
+ '//lib:guava',
+ '//lib:junit',
+ ],
+)
+
+java_library(
+ name = 'project',
+ srcs = [
+ 'ProjectAssert.java',
+ 'ProjectInfo.java',
+ ],
+ deps = [
+ '//gerrit-extension-api:api',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//lib:gwtorm',
+ '//lib:guava',
+ '//lib:junit',
+ ],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
new file mode 100644
index 0000000..654ef65
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchAssert.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+public class BranchAssert {
+
+ public static void assertBranches(List<BranchInfo> expectedBranches,
+ List<BranchInfo> actualBranches) {
+ List<BranchInfo> missingBranches = Lists.newArrayList(actualBranches);
+ for (final BranchInfo b : expectedBranches) {
+ BranchInfo info =
+ Iterables.find(actualBranches, new Predicate<BranchInfo>() {
+ @Override
+ public boolean apply(BranchInfo info) {
+ return info.ref.equals(b.ref);
+ }
+ }, null);
+ assertNotNull("missing branch: " + b.ref, info);
+ assertBranchInfo(b, info);
+ missingBranches.remove(info);
+ }
+ assertTrue("unexpected branches: " + missingBranches,
+ missingBranches.isEmpty());
+ }
+
+ public static void assertBranchInfo(BranchInfo expected, BranchInfo actual) {
+ assertEquals(expected.ref, actual.ref);
+ if (expected.revision != null) {
+ assertEquals(expected.revision, actual.revision);
+ }
+ assertEquals(expected.can_delete, toBoolean(actual.can_delete));
+ }
+
+ private static boolean toBoolean(Boolean b) {
+ if (b == null) {
+ return false;
+ }
+ return b.booleanValue();
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchInfo.java
new file mode 100644
index 0000000..2b7933e
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BranchInfo.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+public class BranchInfo {
+ public String ref;
+ public String revision;
+ public Boolean can_delete;
+
+ public BranchInfo() {
+ }
+
+ public BranchInfo(String ref, String revision, boolean canDelete) {
+ this.ref = ref;
+ this.revision = revision;
+ this.can_delete = canDelete;
+ }
+
+ @Override
+ public String toString() {
+ return ref;
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 6afe8e6..44a40d3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -79,7 +79,7 @@
public void setUp() throws Exception {
admin = accounts.create("admin", "admin@example.com", "Administrator",
"Administrators");
- session = new RestSession(admin);
+ session = new RestSession(server, admin);
}
@Test
@@ -210,7 +210,7 @@
public void testCreateProjectWithoutCapability_Forbidden() throws OrmException,
JSchException, IOException {
TestAccount user = accounts.create("user", "user@example.com", "User");
- RestResponse r = (new RestSession(user)).put("/projects/newProject");
+ RestResponse r = new RestSession(server, user).put("/projects/newProject");
assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
index e55c52a..a1ad627 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
@@ -26,7 +26,6 @@
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.git.GarbageCollectionQueue;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -47,9 +46,6 @@
private AllProjectsName allProjects;
@Inject
- private GarbageCollectionQueue gcQueue;
-
- @Inject
private GcAssert gcAssert;
private TestAccount admin;
@@ -63,7 +59,7 @@
accounts.create("admin", "admin@example.com", "Administrator",
"Administrators");
- SshSession sshSession = new SshSession(admin);
+ SshSession sshSession = new SshSession(server, admin);
project1 = new Project.NameKey("p1");
createProject(sshSession, project1.get());
@@ -71,7 +67,7 @@
project2 = new Project.NameKey("p2");
createProject(sshSession, project2.get());
- session = new RestSession(admin);
+ session = new RestSession(server, admin);
}
@Test
@@ -82,7 +78,7 @@
@Test
public void testGcNotAllowed_Forbidden() throws IOException, OrmException, JSchException {
assertEquals(HttpStatus.SC_FORBIDDEN,
- new RestSession(accounts.create("user", "user@example.com", "User"))
+ new RestSession(server, accounts.create("user", "user@example.com", "User"))
.post("/projects/" + allProjects.get() + "/gc").getStatusCode());
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
new file mode 100644
index 0000000..bf8aac1b
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetChildProjectIT.java
@@ -0,0 +1,126 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class GetChildProjectIT extends AbstractDaemonTest {
+
+ @Inject
+ private AccountCreator accounts;
+
+ @Inject
+ private AllProjectsName allProjects;
+
+ @Inject
+ private ProjectCache projectCache;
+
+ private TestAccount admin;
+ private RestSession session;
+
+ @Before
+ public void setUp() throws Exception {
+ admin =
+ accounts.create("admin", "admin@example.com", "Administrator",
+ "Administrators");
+ session = new RestSession(server, admin);
+ }
+
+ @Test
+ public void getNonExistingChildProject_NotFound() throws IOException {
+ assertEquals(HttpStatus.SC_NOT_FOUND,
+ GET("/projects/" + allProjects.get() + "/children/non-existing").getStatusCode());
+ }
+
+ @Test
+ public void getNonChildProject_NotFound() throws IOException, JSchException {
+ SshSession sshSession = new SshSession(server, admin);
+ Project.NameKey p1 = new Project.NameKey("p1");
+ createProject(sshSession, p1.get());
+ Project.NameKey p2 = new Project.NameKey("p2");
+ createProject(sshSession, p2.get());
+ assertEquals(HttpStatus.SC_NOT_FOUND,
+ GET("/projects/" + p1.get() + "/children/" + p2.get()).getStatusCode());
+ }
+
+ @Test
+ public void getChildProject() throws IOException, JSchException {
+ SshSession sshSession = new SshSession(server, admin);
+ Project.NameKey child = new Project.NameKey("p1");
+ createProject(sshSession, child.get());
+ RestResponse r = GET("/projects/" + allProjects.get() + "/children/" + child.get());
+ assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+ ProjectInfo childInfo =
+ (new Gson()).fromJson(r.getReader(),
+ new TypeToken<ProjectInfo>() {}.getType());
+ assertProjectInfo(projectCache.get(child).getProject(), childInfo);
+ }
+
+ @Test
+ public void getGrandChildProject_NotFound() throws IOException, JSchException {
+ SshSession sshSession = new SshSession(server, admin);
+ Project.NameKey child = new Project.NameKey("p1");
+ createProject(sshSession, child.get());
+ Project.NameKey grandChild = new Project.NameKey("p1.1");
+ createProject(sshSession, grandChild.get(), child);
+ assertEquals(HttpStatus.SC_NOT_FOUND,
+ GET("/projects/" + allProjects.get() + "/children/" + grandChild.get())
+ .getStatusCode());
+ }
+
+ @Test
+ public void getGrandChildProjectWithRecursiveFlag() throws IOException,
+ JSchException {
+ SshSession sshSession = new SshSession(server, admin);
+ Project.NameKey child = new Project.NameKey("p1");
+ createProject(sshSession, child.get());
+ Project.NameKey grandChild = new Project.NameKey("p1.1");
+ createProject(sshSession, grandChild.get(), child);
+ RestResponse r =
+ GET("/projects/" + allProjects.get() + "/children/" + grandChild.get()
+ + "?recursive");
+ assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+ ProjectInfo grandChildInfo =
+ (new Gson()).fromJson(r.getReader(), new TypeToken<ProjectInfo>() {}.getType());
+ assertProjectInfo(projectCache.get(grandChild).getProject(), grandChildInfo);
+ }
+
+ private RestResponse GET(String endpoint) throws IOException {
+ return session.get(endpoint);
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
new file mode 100644
index 0000000..4778662
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -0,0 +1,227 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertBranches;
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+public class ListBranchesIT extends AbstractDaemonTest {
+
+ @Inject
+ private AccountCreator accounts;
+
+ @Inject
+ private MetaDataUpdate.Server metaDataUpdateFactory;
+
+ @Inject
+ private ProjectCache projectCache;
+
+ @Inject
+ private GroupCache groupCache;
+
+ @Inject
+ private SchemaFactory<ReviewDb> reviewDbProvider;
+
+ private TestAccount admin;
+ private RestSession session;
+ private SshSession sshSession;
+ private Project.NameKey project;
+ private Git git;
+ private ReviewDb db;
+
+ @Before
+ public void setUp() throws Exception {
+ admin = accounts.create("admin", "admin@example.com", "Administrator",
+ "Administrators");
+ session = new RestSession(server, admin);
+
+ project = new Project.NameKey("p");
+ initSsh(admin);
+ sshSession = new SshSession(server, admin);
+ createProject(sshSession, project.get());
+ git = cloneProject(sshSession.getUrl() + "/" + project.get());
+ db = reviewDbProvider.open();
+ }
+
+ @After
+ public void cleanup() {
+ db.close();
+ }
+
+ @Test
+ public void listBranchesOfNonExistingProject_NotFound() throws IOException {
+ assertEquals(HttpStatus.SC_NOT_FOUND,
+ GET("/projects/non-existing/branches").getStatusCode());
+ }
+
+ @Test
+ public void listBranchesOfNonVisibleProject_NotFound() throws IOException,
+ OrmException, JSchException, ConfigInvalidException {
+ blockRead(project, "refs/*");
+ RestSession session =
+ new RestSession(server, accounts.create("user", "user@example.com", "User"));
+ assertEquals(HttpStatus.SC_NOT_FOUND,
+ session.get("/projects/" + project.get() + "/branches").getStatusCode());
+ }
+
+ @Test
+ public void listBranchesOfEmptyProject() throws IOException, JSchException {
+ Project.NameKey emptyProject = new Project.NameKey("empty");
+ createProject(sshSession, emptyProject.get(), null, false);
+ RestResponse r = session.get("/projects/" + emptyProject.get() + "/branches");
+ List<BranchInfo> result =
+ (new Gson()).fromJson(r.getReader(),
+ new TypeToken<List<BranchInfo>>() {}.getType());
+ List<BranchInfo> expected = Lists.asList(
+ new BranchInfo("refs/meta/config", null, false),
+ new BranchInfo[] {
+ new BranchInfo("HEAD", null, false)
+ });
+ assertBranches(expected, result);
+ }
+
+ @Test
+ public void listBranches() throws IOException, GitAPIException {
+ pushTo("refs/heads/master");
+ String masterCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
+ pushTo("refs/heads/dev");
+ String devCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
+ RestResponse r = session.get("/projects/" + project.get() + "/branches");
+ List<BranchInfo> result =
+ (new Gson()).fromJson(r.getReader(),
+ new TypeToken<List<BranchInfo>>() {}.getType());
+ List<BranchInfo> expected = Lists.asList(
+ new BranchInfo("refs/meta/config", null, false),
+ new BranchInfo[] {
+ new BranchInfo("HEAD", "master", false),
+ new BranchInfo("refs/heads/master", masterCommit, false),
+ new BranchInfo("refs/heads/dev", devCommit, true)
+ });
+ assertBranches(expected, result);
+
+ // verify correct sorting
+ assertEquals("HEAD", result.get(0).ref);
+ assertEquals("refs/meta/config", result.get(1).ref);
+ assertEquals("refs/heads/dev", result.get(2).ref);
+ assertEquals("refs/heads/master", result.get(3).ref);
+ }
+
+ @Test
+ public void listBranchesSomeHidden() throws IOException, GitAPIException,
+ ConfigInvalidException, OrmException, JSchException {
+ blockRead(project, "refs/heads/dev");
+ RestSession session =
+ new RestSession(server, accounts.create("user", "user@example.com", "User"));
+ pushTo("refs/heads/master");
+ String masterCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
+ pushTo("refs/heads/dev");
+ RestResponse r = session.get("/projects/" + project.get() + "/branches");
+ List<BranchInfo> result =
+ (new Gson()).fromJson(r.getReader(),
+ new TypeToken<List<BranchInfo>>() {}.getType());
+ // refs/meta/config is hidden since user is no project owner
+ List<BranchInfo> expected = Lists.asList(
+ new BranchInfo("HEAD", "master", false),
+ new BranchInfo[] {
+ new BranchInfo("refs/heads/master", masterCommit, false),
+ });
+ assertBranches(expected, result);
+ }
+
+ @Test
+ public void listBranchesHeadHidden() throws IOException, GitAPIException,
+ ConfigInvalidException, OrmException, JSchException {
+ blockRead(project, "refs/heads/master");
+ RestSession session =
+ new RestSession(server, accounts.create("user", "user@example.com", "User"));
+ pushTo("refs/heads/master");
+ pushTo("refs/heads/dev");
+ String devCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
+ RestResponse r = session.get("/projects/" + project.get() + "/branches");
+ List<BranchInfo> result =
+ (new Gson()).fromJson(r.getReader(),
+ new TypeToken<List<BranchInfo>>() {}.getType());
+ // refs/meta/config is hidden since user is no project owner
+ assertBranches(Collections.singletonList(new BranchInfo("refs/heads/dev",
+ devCommit, false)), result);
+ }
+
+ private RestResponse GET(String endpoint) throws IOException {
+ return session.get(endpoint);
+ }
+
+ private void blockRead(Project.NameKey project, String ref)
+ throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+ MetaDataUpdate md = metaDataUpdateFactory.create(project);
+ md.setMessage("Grant submit on " + ref);
+ ProjectConfig config = ProjectConfig.read(md);
+ AccessSection s = config.getAccessSection(ref, true);
+ Permission p = s.getPermission(Permission.READ, true);
+ AccountGroup adminGroup = groupCache.get(AccountGroup.REGISTERED_USERS);
+ PermissionRule rule = new PermissionRule(config.resolve(adminGroup));
+ rule.setBlock();
+ p.add(rule);
+ config.commit(md);
+ projectCache.evict(config.getProject());
+ }
+
+ private PushOneCommit.Result pushTo(String ref) throws GitAPIException,
+ IOException {
+ PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+ return push.to(git, ref);
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
new file mode 100644
index 0000000..c03ecd3
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
@@ -0,0 +1,122 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjects;
+
+import com.google.gson.reflect.TypeToken;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gson.Gson;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+public class ListChildProjectsIT extends AbstractDaemonTest {
+
+ @Inject
+ private AccountCreator accounts;
+
+ @Inject
+ private AllProjectsName allProjects;
+
+ private TestAccount admin;
+ private RestSession session;
+
+ @Before
+ public void setUp() throws Exception {
+ admin =
+ accounts.create("admin", "admin@example.com", "Administrator",
+ "Administrators");
+ session = new RestSession(server, admin);
+ }
+
+ @Test
+ public void listChildrenOfNonExistingProject_NotFound() throws IOException {
+ assertEquals(HttpStatus.SC_NOT_FOUND,
+ GET("/projects/non-existing/children/").getStatusCode());
+ }
+
+ @Test
+ public void listNoChildren() throws IOException {
+ RestResponse r = GET("/projects/" + allProjects.get() + "/children/");
+ assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+ List<ProjectInfo> children =
+ (new Gson()).fromJson(r.getReader(),
+ new TypeToken<List<ProjectInfo>>() {}.getType());
+ assertTrue(children.isEmpty());
+ }
+
+ @Test
+ public void listChildren() throws IOException, JSchException {
+ SshSession sshSession = new SshSession(server, admin);
+ Project.NameKey child1 = new Project.NameKey("p1");
+ createProject(sshSession, child1.get());
+ Project.NameKey child2 = new Project.NameKey("p2");
+ createProject(sshSession, child2.get());
+ createProject(sshSession, "p1.1", child1);
+
+ RestResponse r = GET("/projects/" + allProjects.get() + "/children/");
+ assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+ List<ProjectInfo> children =
+ (new Gson()).fromJson(r.getReader(),
+ new TypeToken<List<ProjectInfo>>() {}.getType());
+ assertProjects(Arrays.asList(child1, child2), children);
+ }
+
+ @Test
+ public void listChildrenRecursively() throws IOException, JSchException {
+ SshSession sshSession = new SshSession(server, admin);
+ Project.NameKey child1 = new Project.NameKey("p1");
+ createProject(sshSession, child1.get());
+ createProject(sshSession, "p2");
+ Project.NameKey child1_1 = new Project.NameKey("p1.1");
+ createProject(sshSession, child1_1.get(), child1);
+ Project.NameKey child1_2 = new Project.NameKey("p1.2");
+ createProject(sshSession, child1_2.get(), child1);
+ Project.NameKey child1_1_1 = new Project.NameKey("p1.1.1");
+ createProject(sshSession, child1_1_1.get(), child1_1);
+ Project.NameKey child1_1_1_1 = new Project.NameKey("p1.1.1.1");
+ createProject(sshSession, child1_1_1_1.get(), child1_1_1);
+
+ RestResponse r = GET("/projects/" + child1.get() + "/children/?recursive");
+ assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+ List<ProjectInfo> children =
+ (new Gson()).fromJson(r.getReader(),
+ new TypeToken<List<ProjectInfo>>() {}.getType());
+ assertProjects(Arrays.asList(child1_1, child1_2, child1_1_1, child1_1_1_1), children);
+ }
+
+ private RestResponse GET(String endpoint) throws IOException {
+ return session.get(endpoint);
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
index 25ccbee..224d59d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
@@ -15,18 +15,36 @@
package com.google.gerrit.acceptance.rest.project;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import com.google.common.base.Predicate;
import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.project.ProjectState;
+import java.util.List;
import java.util.Set;
public class ProjectAssert {
+ public static void assertProjects(Iterable<Project.NameKey> expected,
+ List<ProjectInfo> actual) {
+ for (final Project.NameKey p : expected) {
+ ProjectInfo info = Iterables.find(actual, new Predicate<ProjectInfo>() {
+ @Override
+ public boolean apply(ProjectInfo info) {
+ return new Project.NameKey(info.name).equals(p);
+ }}, null);
+ assertNotNull("missing project: " + p, info);
+ actual.remove(info);
+ }
+ assertTrue("unexpected projects: " + actual, actual.isEmpty());
+ }
+
public static void assertProjectInfo(Project project, ProjectInfo info) {
if (info.name != null) {
// 'name' is not set if returned in a map
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java
index 72dd2d6..2e209d1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java
@@ -19,4 +19,9 @@
public String name;
public String parent;
public String description;
+
+ @Override
+ public String toString() {
+ return name;
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
new file mode 100644
index 0000000..94e6f6a
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
@@ -0,0 +1,6 @@
+include_defs('//gerrit-acceptance-tests/tests.defs')
+
+acceptance_tests(
+ srcs = glob(['*IT.java']),
+ deps = ['//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:util'],
+)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
index 9c1e7d0..e93973b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -70,7 +70,7 @@
accounts.create("admin", "admin@example.com", "Administrator",
"Administrators");
- sshSession = new SshSession(admin);
+ sshSession = new SshSession(server, admin);
project1 = new Project.NameKey("p1");
createProject(sshSession, project1.get());
@@ -104,9 +104,9 @@
@Test
public void testGcWithoutCapability_Error() throws IOException, OrmException,
JSchException {
- SshSession s = new SshSession(accounts.create("user", "user@example.com", "User"));
+ SshSession s = new SshSession(server, accounts.create("user", "user@example.com", "User"));
s.exec("gerrit gc --all");
- assertError("fatal: user does not have \"runGC\" capability.", s.getError());
+ assertError("Capability runGC is required to access this resource", s.getError());
}
@Test
diff --git a/gerrit-acceptance-tests/tests.defs b/gerrit-acceptance-tests/tests.defs
new file mode 100644
index 0000000..f125a39
--- /dev/null
+++ b/gerrit-acceptance-tests/tests.defs
@@ -0,0 +1,20 @@
+def acceptance_tests(
+ srcs,
+ deps = [],
+ vm_args = ['-Xmx128m']):
+ for j in srcs:
+ java_test(
+ name = j[:-len('.java')],
+ srcs = [j],
+ deps = ['//gerrit-acceptance-tests:lib'] + deps,
+ source_under_test = [
+ '//gerrit-httpd:httpd',
+ '//gerrit-sshd:sshd',
+ '//gerrit-server:server',
+ ],
+ labels = [
+ 'acceptance',
+ 'slow',
+ ],
+ vm_args = vm_args,
+ )
diff --git a/gerrit-antlr/BUCK b/gerrit-antlr/BUCK
new file mode 100644
index 0000000..57e7e1d
--- /dev/null
+++ b/gerrit-antlr/BUCK
@@ -0,0 +1,36 @@
+PARSER_DEPS = [
+ ':query_exception',
+ '//lib/antlr:java_runtime',
+]
+
+java_library(
+ name = 'query_exception',
+ srcs = ['src/main/java/com/google/gerrit/server/query/QueryParseException.java'],
+ visibility = ['PUBLIC'],
+)
+
+genantlr(
+ name = 'query_antlr',
+ srcs = ['src/main/antlr3/com/google/gerrit/server/query/Query.g'],
+ out = 'query_antlr.src.zip',
+)
+
+java_library(
+ name = 'lib',
+ srcs = [genfile('query_antlr.src.zip')],
+ deps = PARSER_DEPS + [':query_antlr'],
+)
+
+# Hack necessary to expose ANTLR generated code as JAR to Eclipse.
+genrule(
+ name = 'query_link',
+ cmd = 'ln -s $(location :lib) $OUT',
+ deps = [':lib'],
+ out = 'query_parser.jar',
+)
+prebuilt_jar(
+ name = 'query_parser',
+ binary_jar = genfile('query_parser.jar'),
+ deps = PARSER_DEPS + [':query_link'],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-antlr/pom.xml b/gerrit-antlr/pom.xml
deleted file mode 100644
index c17e01e..0000000
--- a/gerrit-antlr/pom.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-antlr</artifactId>
- <name>Gerrit Code Review - ANTLR</name>
-
- <description>
- ANTLR generated sources
- </description>
-
- <dependencies>
- <dependency>
- <groupId>org.antlr</groupId>
- <artifactId>antlr</artifactId>
- </dependency>
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.antlr</groupId>
- <artifactId>antlr3-maven-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>antlr</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-source-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>jar</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/gerrit-antlr/src/main/java/com/google/gerrit/server/query/QueryParseException.java b/gerrit-antlr/src/main/java/com/google/gerrit/server/query/QueryParseException.java
index 97eb2bf..1f69ba7 100644
--- a/gerrit-antlr/src/main/java/com/google/gerrit/server/query/QueryParseException.java
+++ b/gerrit-antlr/src/main/java/com/google/gerrit/server/query/QueryParseException.java
@@ -14,6 +14,11 @@
package com.google.gerrit.server.query;
+/**
+ * Exception thrown when a search query is invalid.
+ * <p>
+ * <b>NOTE:</b> the message is visible to end users.
+ */
public class QueryParseException extends Exception {
private static final long serialVersionUID = 1L;
diff --git a/gerrit-cache-h2/BUCK b/gerrit-cache-h2/BUCK
new file mode 100644
index 0000000..d3e8994
--- /dev/null
+++ b/gerrit-cache-h2/BUCK
@@ -0,0 +1,14 @@
+java_library(
+ name = 'cache-h2',
+ srcs = glob(['src/main/java/**/*.java']),
+ deps = [
+ '//gerrit-extension-api:api',
+ '//gerrit-server:server',
+ '//lib:guava',
+ '//lib:h2',
+ '//lib/guice:guice',
+ '//lib/jgit:jgit',
+ '//lib/log:api',
+ ],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-cache-h2/pom.xml b/gerrit-cache-h2/pom.xml
deleted file mode 100644
index 80ab03b..0000000
--- a/gerrit-cache-h2/pom.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2012 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-cache-h2</artifactId>
- <name>Gerrit Code Review - Guava + H2 caching</name>
-
- <description>
- Implementation of caching backed by Guava and H2
- </description>
-
- <dependencies>
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-server</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- </dependency>
- </dependencies>
-</project>
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
index 5a600c0..1c850d1 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
@@ -85,7 +85,7 @@
"cache", def.name(), "memoryLimit",
def.maximumWeight()));
- builder.removalListener(forwardingRemovalListenerFactory.create(def.name()));
+ builder = builder.removalListener(forwardingRemovalListenerFactory.create(def.name()));
Weigher<K, V> weigher = def.weigher();
if (weigher != null && unwrapValueHolder) {
diff --git a/gerrit-common/BUCK b/gerrit-common/BUCK
new file mode 100644
index 0000000..a79930b
--- /dev/null
+++ b/gerrit-common/BUCK
@@ -0,0 +1,42 @@
+SRC = 'src/main/java/com/google/gerrit/'
+
+gwt_module(
+ name = 'client',
+ srcs = glob([SRC + 'common/**/*.java']),
+ gwtxml = SRC + 'Common.gwt.xml',
+ deps = [
+ '//gerrit-patch-jgit:client',
+ '//gerrit-prettify:client',
+ '//gerrit-reviewdb:client',
+ '//lib:gwtjsonrpc',
+ '//lib:gwtorm',
+ '//lib:jsr305',
+ '//lib/jgit:jgit',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+java_library(
+ name = 'server',
+ srcs = glob([SRC + 'common/**/*.java']),
+ deps = [
+ '//gerrit-patch-jgit:server',
+ '//gerrit-prettify:server',
+ '//gerrit-reviewdb:server',
+ '//lib:gwtjsonrpc',
+ '//lib:gwtorm',
+ '//lib:jsr305',
+ '//lib/jgit:jgit',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+java_test(
+ name = 'client_tests',
+ srcs = glob(['src/test/java/**/*.java']),
+ deps = [
+ ':client',
+ '//lib:junit',
+ ],
+ source_under_test = [':client'],
+)
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
deleted file mode 100644
index efa7285..0000000
--- a/gerrit-common/pom.xml
+++ /dev/null
@@ -1,112 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-common</artifactId>
- <name>Gerrit Code Review - Common</name>
-
- <description>
- Classes common to both server and client.
- </description>
-
- <dependencies>
- <dependency>
- <groupId>com.google.gwt</groupId>
- <artifactId>gwt-servlet</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-gwtexpui</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-reviewdb</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-prettify</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-patch-jgit</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.code.findbugs</groupId>
- <artifactId>jsr305</artifactId>
- </dependency>
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-antrun-plugin</artifactId>
- <executions>
- <execution>
- <id>generate-version</id>
- <phase>generate-resources</phase>
- <configuration>
- <target>
- <property name="dst" location="${project.build.outputDirectory}" />
- <property name="pkg" location="${dst}/com/google/gerrit/common" />
- <mkdir dir="${pkg}" />
- <exec executable="git" outputproperty="v">
- <arg value="describe"/>
- <arg value="HEAD"/>
- </exec>
- <echo file="${pkg}/Version">${v}</echo>
- </target>
- </configuration>
- <goals>
- <goal>run</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-source-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>jar</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java b/gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java
deleted file mode 100644
index 42ee5dd..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/ClientVersion.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common;
-
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.TextResource;
-
-public interface ClientVersion extends ClientBundle {
- /** Version number of this client software build. */
- @Source("Version")
- TextResource version();
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index beab1d9..ad415d4 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -54,6 +54,14 @@
return "/c/" + c + "/";
}
+ public static String toChange2(final Change.Id c) {
+ return "/c2/" + c + "/";
+ }
+
+ public static String toChange2(Change.Id c, String p) {
+ return "/c2/" + c + "/" + p;
+ }
+
public static String toChange(final PatchSet.Id ps) {
return "/c/" + ps.getParentKey() + "/" + ps.get();
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java b/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
index f5ef9c5..94d8389 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
@@ -37,7 +37,13 @@
DETAILED_ACCOUNTS(7),
/** Include messages associated with the change. */
- MESSAGES(9);
+ MESSAGES(9),
+
+ /** Include allowed actions client could perform. */
+ CURRENT_ACTIONS(10),
+
+ /** Set the reviewed boolean for the caller. */
+ REVIEWED(11);
private final int value;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/changes/Side.java b/gerrit-common/src/main/java/com/google/gerrit/common/changes/Side.java
new file mode 100644
index 0000000..777467d
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/changes/Side.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.changes;
+
+/** The side on which a comment was added. */
+public enum Side {
+ PARENT, REVISION;
+}
\ No newline at end of file
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
index c2d03ab..f382d49 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
@@ -18,7 +18,6 @@
import com.google.gerrit.common.auth.SignInRequired;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.client.ContactInformation;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -31,32 +30,10 @@
@RpcImpl(version = Version.V2_0)
public interface AccountSecurity extends RemoteJsonService {
- @SignInRequired
- void mySshKeys(AsyncCallback<List<AccountSshKey>> callback);
-
- @Audit
- @SignInRequired
- void addSshKey(String keyText, AsyncCallback<AccountSshKey> callback);
-
- @Audit
- @SignInRequired
- void deleteSshKeys(Set<AccountSshKey.Id> ids,
- AsyncCallback<VoidResult> callback);
-
@Audit
@SignInRequired
void changeUserName(String newName, AsyncCallback<VoidResult> callback);
- @Audit
- @SignInRequired
- void generatePassword(AccountExternalId.Key key,
- AsyncCallback<AccountExternalId> callback);
-
- @Audit
- @SignInRequired
- void clearPassword(AccountExternalId.Key key,
- AsyncCallback<AccountExternalId> gerritCallback);
-
@SignInRequired
void myExternalIds(AsyncCallback<List<AccountExternalId>> callback);
@@ -77,9 +54,5 @@
@Audit
@SignInRequired
- void registerEmail(String address, AsyncCallback<Account> callback);
-
- @Audit
- @SignInRequired
void validateEmail(String token, AsyncCallback<VoidResult> callback);
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AddBranchResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AddBranchResult.java
deleted file mode 100644
index 24e527dc..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AddBranchResult.java
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-public class AddBranchResult {
- protected ListBranchesResult listBranchesResult;
- protected Error error;
-
- protected AddBranchResult() {
- }
-
- public AddBranchResult(final Error error) {
- this.error = error;
- }
-
- public AddBranchResult(final ListBranchesResult listBranchesResult) {
- this.listBranchesResult = listBranchesResult;
- }
-
- public ListBranchesResult getListBranchesResult() {
- return listBranchesResult;
- }
-
- public boolean hasError() {
- return error != null;
- }
-
- public Error getError() {
- return error;
- }
-
- public static class Error {
- public static enum Type {
- /** The branch cannot be created because the given branch name is invalid. */
- INVALID_NAME,
-
- /** The branch cannot be created because the given revision is invalid. */
- INVALID_REVISION,
-
- /**
- * The branch cannot be created under the given refname prefix (e.g
- * branches cannot be created under magic refname prefixes).
- */
- BRANCH_CREATION_NOT_ALLOWED_UNDER_REFNAME_PREFIX,
-
- /** The branch that should be created exists already. */
- BRANCH_ALREADY_EXISTS,
-
- /**
- * The branch cannot be created because it conflicts with an existing
- * branch (branches cannot be nested).
- */
- BRANCH_CREATION_CONFLICT
- }
-
- protected Type type;
- protected String refname;
-
- protected Error() {
- }
-
- public Error(final Type type) {
- this(type, null);
- }
-
- public Error(final Type type, final String refname) {
- this.type = type;
- this.refname = refname;
- }
-
- public Type getType() {
- return type;
- }
-
- public String getRefname() {
- return refname;
- }
-
- @Override
- public String toString() {
- return type + " " + refname;
- }
- }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
index f6d5ea33..3342bc2 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
@@ -28,6 +28,7 @@
protected boolean allowsAnonymous;
protected boolean canAbandon;
protected boolean canEditCommitMessage;
+ protected boolean canCherryPick;
protected boolean canPublish;
protected boolean canRebase;
protected boolean canRestore;
@@ -84,6 +85,14 @@
canEditCommitMessage = a;
}
+ public boolean canCherryPick() {
+ return canCherryPick;
+ }
+
+ public void setCanCherryPick(final boolean a) {
+ canCherryPick = a;
+ }
+
public boolean canPublish() {
return canPublish;
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
index 7660a80..f85e333 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
@@ -26,8 +26,12 @@
public class GerritConfig implements Cloneable {
protected String registerUrl;
protected String registerText;
+ protected String loginUrl;
+ protected String loginText;
+ protected String switchAccountUrl;
protected String httpPasswordUrl;
protected String reportBugUrl;
+ protected boolean gitBasicAuth;
protected GitwebConfig gitweb;
protected boolean useContributorAgreements;
@@ -46,6 +50,23 @@
protected boolean testChangeMerge;
protected String anonymousCowardName;
protected int suggestFrom;
+ protected int changeUpdateDelay;
+
+ public String getLoginUrl() {
+ return loginUrl;
+ }
+
+ public void setLoginUrl(final String u) {
+ loginUrl = u;
+ }
+
+ public String getLoginText() {
+ return loginText;
+ }
+
+ public void setLoginText(String signinText) {
+ this.loginText = signinText;
+ }
public String getRegisterUrl() {
return registerUrl;
@@ -55,6 +76,14 @@
registerUrl = u;
}
+ public String getSwitchAccountUrl() {
+ return switchAccountUrl;
+ }
+
+ public void setSwitchAccountUrl(String u) {
+ switchAccountUrl = u;
+ }
+
public String getRegisterText() {
return registerText;
}
@@ -71,6 +100,14 @@
reportBugUrl = u;
}
+ public boolean isGitBasicAuth() {
+ return gitBasicAuth;
+ }
+
+ public void setGitBasicAuth(boolean gba) {
+ gitBasicAuth = gba;
+ }
+
public String getEditFullNameUrl() {
return editFullNameUrl;
}
@@ -225,4 +262,12 @@
}
return true;
}
+
+ public int getChangeUpdateDelay() {
+ return changeUpdateDelay;
+ }
+
+ public void setChangeUpdateDelay(int seconds) {
+ changeUpdateDelay = seconds;
+ }
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index 8c08feb..1fd98b6 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -58,6 +58,9 @@
/** Can flush any cache except the active web_sessions cache. */
public static final String FLUSH_CACHES = "flushCaches";
+ /** Can generate HTTP passwords for user other than self. */
+ public static final String GENERATE_HTTP_PASSWORD = "generateHttpPassword";
+
/** Can terminate any task using the kill command. */
public static final String KILL_TASK = "killTask";
@@ -67,12 +70,12 @@
/** Maximum result limit per executed query. */
public static final String QUERY_LIMIT = "queryLimit";
+ /** Ability to impersonate another user. */
+ public static final String RUN_AS = "runAs";
+
/** Can run the Git garbage collection. */
public static final String RUN_GC = "runGC";
- /** Forcefully restart replication to any configured destination. */
- public static final String START_REPLICATION = "startReplication";
-
/** Can perform streaming of Gerrit events. */
public static final String STREAM_EVENTS = "streamEvents";
@@ -100,8 +103,8 @@
NAMES_ALL.add(KILL_TASK);
NAMES_ALL.add(PRIORITY);
NAMES_ALL.add(QUERY_LIMIT);
+ NAMES_ALL.add(RUN_AS);
NAMES_ALL.add(RUN_GC);
- NAMES_ALL.add(START_REPLICATION);
NAMES_ALL.add(STREAM_EVENTS);
NAMES_ALL.add(VIEW_CACHES);
NAMES_ALL.add(VIEW_CONNECTIONS);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
index 0d2be51..c890812 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
@@ -15,7 +15,7 @@
package com.google.gerrit.common.data;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import java.util.List;
@@ -24,7 +24,7 @@
public AccountInfoCache accounts;
public AccountGroup group;
public List<AccountGroupMember> members;
- public List<AccountGroupIncludeByUuid> includes;
+ public List<AccountGroupById> includes;
public GroupReference ownerGroup;
public boolean canModify;
@@ -43,7 +43,7 @@
members = m;
}
- public void setIncludes(List<AccountGroupIncludeByUuid> i) {
+ public void setIncludes(List<AccountGroupById> i) {
includes = i;
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
index f014f5f..f143405 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
@@ -21,6 +21,7 @@
/** Data sent as part of the host page, to bootstrap the UI. */
public class HostPageData {
+ public String version;
public Account account;
public AccountDiffPreference accountDiffPref;
public String xGerritAuth;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
index db8bde9..caf914d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
@@ -103,6 +103,7 @@
protected short maxPositive;
private transient boolean canOverride;
+ private transient List<String> refPatterns;
private transient List<Integer> intList;
private transient Map<Short, LabelValue> byValue;
@@ -157,10 +158,18 @@
return canOverride;
}
+ public List<String> getRefPatterns() {
+ return refPatterns;
+ }
+
public void setCanOverride(boolean canOverride) {
this.canOverride = canOverride;
}
+ public void setRefPatterns(List<String> refPatterns) {
+ this.refPatterns = refPatterns;
+ }
+
public List<LabelValue> getValues() {
return values;
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java
deleted file mode 100644
index 1c830e9..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.reviewdb.client.Branch;
-
-import java.util.List;
-
-/**
- * It holds list of branches and boolean to indicate if it is allowed to add new
- * branches.
- */
-public final class ListBranchesResult {
- protected boolean noRepository;
- protected boolean canAdd;
- protected List<Branch> branches;
-
- protected ListBranchesResult() {
- }
-
- public ListBranchesResult(List<Branch> branches, boolean canAdd,
- boolean noRepository) {
- this.branches = branches;
- this.canAdd = canAdd;
- this.noRepository = noRepository;
- }
-
- public boolean getNoRepository() {
- return noRepository;
- }
-
- public boolean getCanAdd() {
- return canAdd;
- }
-
- public List<Branch> getBranches() {
- return branches;
- }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index fecbb76..914e69f 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -48,6 +48,8 @@
protected List<Edit> edits;
protected DisplayMethod displayMethodA;
protected DisplayMethod displayMethodB;
+ protected transient String mimeTypeA;
+ protected transient String mimeTypeB;
protected CommentDetail comments;
protected List<Patch> history;
protected boolean hugeFile;
@@ -60,8 +62,9 @@
final List<String> h, final AccountDiffPreference dp,
final SparseFileContent ca, final SparseFileContent cb,
final List<Edit> e, final DisplayMethod ma, final DisplayMethod mb,
- final CommentDetail cd, final List<Patch> hist, final boolean hf,
- final boolean id, final boolean idf, final boolean idt) {
+ final String mta, final String mtb, final CommentDetail cd,
+ final List<Patch> hist, final boolean hf, final boolean id,
+ final boolean idf, final boolean idt) {
changeId = ck;
changeType = ct;
oldName = on;
@@ -75,6 +78,8 @@
edits = e;
displayMethodA = ma;
displayMethodB = mb;
+ mimeTypeA = mta;
+ mimeTypeB = mtb;
comments = cd;
history = hist;
hugeFile = hf;
@@ -170,6 +175,14 @@
return b;
}
+ public String getMimeTypeA() {
+ return mimeTypeA;
+ }
+
+ public String getMimeTypeB() {
+ return mimeTypeB;
+ }
+
public List<Edit> getEdits() {
return edits;
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
index 39f5cb0..9f4da74 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
@@ -19,6 +19,7 @@
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.Project;
+import java.util.Collections;
import java.util.List;
public class PatchSetDetail {
@@ -26,6 +27,7 @@
protected PatchSetInfo info;
protected List<Patch> patches;
protected Project.NameKey project;
+ protected List<UiCommandDetail> commands;
public PatchSetDetail() {
}
@@ -61,4 +63,15 @@
public void setProject(final Project.NameKey p) {
project = p;
}
+
+ public List<UiCommandDetail> getCommands() {
+ if (commands != null) {
+ return commands;
+ }
+ return Collections.emptyList();
+ }
+
+ public void setCommands(List<UiCommandDetail> cmds) {
+ commands = cmds.isEmpty() ? null : cmds;
+ }
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
index 43fefba..69263d9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -29,6 +29,7 @@
public static final String FORGE_COMMITTER = "forgeCommitter";
public static final String FORGE_SERVER = "forgeServerAsCommitter";
public static final String LABEL = "label-";
+ public static final String LABEL_AS = "labelAs-";
public static final String OWNER = "owner";
public static final String PUBLISH_DRAFTS = "publishDrafts";
public static final String PUSH = "push";
@@ -43,6 +44,7 @@
private static final List<String> NAMES_LC;
private static final int labelIndex;
+ private static final int labelAsIndex;
static {
NAMES_LC = new ArrayList<String>();
@@ -58,6 +60,7 @@
NAMES_LC.add(PUSH_TAG.toLowerCase());
NAMES_LC.add(PUSH_SIGNED_TAG.toLowerCase());
NAMES_LC.add(LABEL.toLowerCase());
+ NAMES_LC.add(LABEL_AS.toLowerCase());
NAMES_LC.add(REBASE.toLowerCase());
NAMES_LC.add(REMOVE_REVIEWER.toLowerCase());
NAMES_LC.add(SUBMIT.toLowerCase());
@@ -67,15 +70,18 @@
NAMES_LC.add(PUBLISH_DRAFTS.toLowerCase());
labelIndex = NAMES_LC.indexOf(Permission.LABEL);
+ labelAsIndex = NAMES_LC.indexOf(Permission.LABEL_AS.toLowerCase());
}
/** @return true if the name is recognized as a permission name. */
public static boolean isPermission(String varName) {
- String lc = varName.toLowerCase();
- if (lc.startsWith(LABEL)) {
- return LABEL.length() < lc.length();
- }
- return NAMES_LC.contains(lc);
+ return isLabel(varName)
+ || isLabelAs(varName)
+ || NAMES_LC.contains(varName.toLowerCase());
+ }
+
+ public static boolean hasRange(String varName) {
+ return isLabel(varName) || isLabelAs(varName);
}
/** @return true if the permission name is actually for a review label. */
@@ -83,11 +89,30 @@
return varName.startsWith(LABEL) && LABEL.length() < varName.length();
}
+ /** @return true if the permission is for impersonated review labels. */
+ public static boolean isLabelAs(String var) {
+ return var.startsWith(LABEL_AS) && LABEL_AS.length() < var.length();
+ }
+
/** @return permission name for the given review label. */
public static String forLabel(String labelName) {
return LABEL + labelName;
}
+ /** @return permission name to apply a label for another user. */
+ public static String forLabelAs(String labelName) {
+ return LABEL_AS + labelName;
+ }
+
+ public static String extractLabel(String varName) {
+ if (isLabel(varName)) {
+ return varName.substring(LABEL.length());
+ } else if (isLabelAs(varName)) {
+ return varName.substring(LABEL_AS.length());
+ }
+ return null;
+ }
+
public static boolean canBeOnAllProjects(String ref, String permissionName) {
if (AccessSection.ALL.equals(ref)) {
return !OWNER.equals(permissionName);
@@ -110,15 +135,8 @@
return name;
}
- public boolean isLabel() {
- return isLabel(getName());
- }
-
public String getLabel() {
- if (isLabel()) {
- return getName().substring(LABEL.length());
- }
- return null;
+ return extractLabel(getName());
}
public Boolean getExclusiveGroup() {
@@ -223,8 +241,10 @@
}
private static int index(Permission a) {
- if (a.isLabel()) {
+ if (isLabel(a.getName())) {
return labelIndex;
+ } else if (isLabelAs(a.getName())) {
+ return labelAsIndex;
}
int index = NAMES_LC.indexOf(a.getName().toLowerCase());
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java
index 3490dd7..0363fd6 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java
@@ -86,7 +86,7 @@
}
public String getLabel() {
- return isLabel() ? getName().substring(Permission.LABEL.length()) : null;
+ return Permission.extractLabel(getName());
}
public int getMin() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
index 904c5c7..ee6cc95 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
@@ -17,6 +17,7 @@
import com.google.gerrit.reviewdb.client.Project;
import java.util.List;
+import java.util.Map;
import java.util.Set;
public class ProjectAccess {
@@ -28,6 +29,7 @@
protected boolean isConfigVisible;
protected boolean canUpload;
protected LabelTypes labelTypes;
+ protected Map<String, String> capabilities;
public ProjectAccess() {
}
@@ -112,4 +114,12 @@
public void setLabelTypes(LabelTypes labelTypes) {
this.labelTypes = labelTypes;
}
+
+ public Map<String, String> getCapabilities() {
+ return capabilities;
+ }
+
+ public void setCapabilities(Map<String, String> capabilities) {
+ this.capabilities = capabilities;
+ }
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
index 0638906..44f60861 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
@@ -16,7 +16,6 @@
import com.google.gerrit.common.audit.Audit;
import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -25,25 +24,14 @@
import com.google.gwtjsonrpc.common.RpcImpl.Version;
import java.util.List;
-import java.util.Set;
@RpcImpl(version = Version.V2_0)
public interface ProjectAdminService extends RemoteJsonService {
- void visibleProjectDetails(AsyncCallback<List<ProjectDetail>> callback);
-
- void projectDetail(Project.NameKey projectName,
- AsyncCallback<ProjectDetail> callback);
-
void projectAccess(Project.NameKey projectName,
AsyncCallback<ProjectAccess> callback);
@Audit
@SignInRequired
- void changeProjectSettings(Project update,
- AsyncCallback<ProjectDetail> callback);
-
- @Audit
- @SignInRequired
void changeProjectAccess(Project.NameKey projectName, String baseRevision,
String message, List<AccessSection> sections,
AsyncCallback<ProjectAccess> callback);
@@ -52,17 +40,4 @@
void reviewProjectAccess(Project.NameKey projectName, String baseRevision,
String message, List<AccessSection> sections,
AsyncCallback<Change.Id> callback);
-
- void listBranches(Project.NameKey projectName,
- AsyncCallback<ListBranchesResult> callback);
-
- @Audit
- @SignInRequired
- void addBranch(Project.NameKey projectName, String branchName,
- String startingRevision, AsyncCallback<AddBranchResult> callback);
-
- @Audit
- @SignInRequired
- void deleteBranch(Project.NameKey projectName, Set<Branch.NameKey> ids,
- AsyncCallback<Set<Branch.NameKey>> callback);
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java
deleted file mode 100644
index 1e7589b..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.reviewdb.client.InheritedBoolean;
-import com.google.gerrit.reviewdb.client.Project;
-
-public class ProjectDetail {
- public Project project;
- public boolean canModifyDescription;
- public boolean canModifyMergeType;
- public boolean canModifyAgreements;
- public boolean canModifyAccess;
- public boolean canModifyState;
- public boolean isPermissionOnly;
- public InheritedBoolean useContributorAgreements;
- public InheritedBoolean useSignedOffBy;
- public InheritedBoolean useContentMerge;
- public InheritedBoolean requireChangeID;
-
- public ProjectDetail() {
- }
-
- public void setProject(final Project p) {
- project = p;
- }
-
- public void setCanModifyDescription(final boolean cmd) {
- canModifyDescription = cmd;
- }
-
- public void setCanModifyMergeType(final boolean cmmt) {
- canModifyMergeType = cmmt;
- }
-
- public void setCanModifyState(final boolean cms) {
- canModifyState = cms;
- }
-
- public void setCanModifyAgreements(final boolean cma) {
- canModifyAgreements = cma;
- }
-
- public void setCanModifyAccess(final boolean cma) {
- canModifyAccess = cma;
- }
-
- public void setPermissionOnly(final boolean ipo) {
- isPermissionOnly = ipo;
- }
-
- public void setUseContributorAgreements(final InheritedBoolean uca) {
- useContributorAgreements = uca;
- }
-
- public void setUseSignedOffBy(final InheritedBoolean usob) {
- useSignedOffBy = usob;
- }
-
- public void setUseContentMerge(final InheritedBoolean ucm) {
- useContentMerge = ucm;
- }
-
- public void setRequireChangeID(final InheritedBoolean rcid) {
- requireChangeID = rcid;
- }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
new file mode 100644
index 0000000..cd011860
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/UiCommandDetail.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+/** Detail necessary to display an action. */
+public class UiCommandDetail {
+ public String id;
+ public String method;
+ public String label;
+ public String title;
+ public boolean enabled;
+}
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
new file mode 100644
index 0000000..4145636
--- /dev/null
+++ b/gerrit-extension-api/BUCK
@@ -0,0 +1,14 @@
+SRCS = glob(['src/main/java/com/google/gerrit/extensions/**/*.java'])
+
+java_library2(
+ name = 'api',
+ srcs = SRCS,
+ compile_deps = ['//lib/guice:guice'],
+ visibility = ['PUBLIC'],
+)
+
+java_sources(
+ name = 'api-src',
+ srcs = SRCS,
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
deleted file mode 100644
index bcdbfab..0000000
--- a/gerrit-extension-api/pom.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2012 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-extension-api</artifactId>
- <name>Gerrit Code Review - Extension API</name>
-
- <description>
- Interfaces describing the extension API
- </description>
-
- <dependencies>
- <dependency>
- <groupId>com.google.inject</groupId>
- <artifactId>guice</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.inject.extensions</groupId>
- <artifactId>guice-servlet</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.apache.tomcat</groupId>
- <artifactId>servlet-api</artifactId>
- </dependency>
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-shade-plugin</artifactId>
- <configuration>
- <createSourcesJar>true</createSourcesJar>
- <shadedArtifactAttached>true</shadedArtifactAttached>
- <shadedClassifierName>all</shadedClassifierName>
- </configuration>
- <executions>
- <execution>
- <phase>package</phase>
- <goals>
- <goal>shade</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/CapabilityScope.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/CapabilityScope.java
new file mode 100644
index 0000000..ede8b8c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/CapabilityScope.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+/** Declared scope of a capability named by {@link RequiresCapability}. */
+public enum CapabilityScope {
+ /**
+ * Scope is assumed based on the context.
+ *
+ * When {@code @RequiresCapability} is used within a plugin the scope of the
+ * capability is assumed to be that plugin.
+ *
+ * If {@code @RequiresCapability} is used within the core Gerrit Code Review
+ * server (and thus is outside of a plugin) the scope is the core server and
+ * will use {@link com.google.gerrit.common.data.GlobalCapability}.
+ */
+ CONTEXT,
+
+ /** Scope is only the plugin. */
+ PLUGIN,
+
+ /** Scope is the core server. */
+ CORE;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
index 382f4ea..b8e07d1 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
@@ -27,5 +27,9 @@
@Target({ElementType.TYPE})
@Retention(RUNTIME)
public @interface RequiresCapability {
+ /** Name of the capability required to invoke this action. */
String value();
+
+ /** Scope of the named capability. */
+ CapabilityScope scope() default CapabilityScope.CONTEXT;
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/CapabilityDefinition.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
new file mode 100644
index 0000000..aafb583
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.config;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/** Specifies a capability declared by a plugin. */
+@ExtensionPoint
+public abstract class CapabilityDefinition {
+ /** @return description of the capability. */
+ public abstract String getDescription();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectDeletedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectDeletedListener.java
new file mode 100644
index 0000000..b03f99c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ProjectDeletedListener.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/** Notified whenever a project is deleted on the master. */
+@ExtensionPoint
+public interface ProjectDeletedListener {
+ public interface Event {
+ String getProjectName();
+ }
+
+ void onProjectDeleted(Event event);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
index 40bbb80..a4c5eb5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -22,6 +22,7 @@
import com.google.inject.util.Types;
import java.util.Collections;
+import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
@@ -39,7 +40,7 @@
* internally, and resolve the provider to an instance on demand. This enables
* registrations to decide between singleton and non-singleton members.
*/
-public abstract class DynamicMap<T> {
+public abstract class DynamicMap<T> implements Iterable<DynamicMap.Entry<T>> {
/**
* Declare a singleton {@code DynamicMap<T>} with a binder.
* <p>
@@ -136,6 +137,50 @@
return Collections.unmodifiableSortedMap(r);
}
+ /** Iterate through all entries in an undefined order. */
+ public Iterator<Entry<T>> iterator() {
+ final Iterator<Map.Entry<NamePair, Provider<T>>> i =
+ items.entrySet().iterator();
+ return new Iterator<Entry<T>>() {
+ @Override
+ public boolean hasNext() {
+ return i.hasNext();
+ }
+
+ @Override
+ public Entry<T> next() {
+ final Map.Entry<NamePair, Provider<T>> e = i.next();
+ return new Entry<T>() {
+ @Override
+ public String getPluginName() {
+ return e.getKey().pluginName;
+ }
+
+ @Override
+ public String getExportName() {
+ return e.getKey().exportName;
+ }
+
+ @Override
+ public Provider<T> getProvider() {
+ return e.getValue();
+ }
+ };
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ public interface Entry<T> {
+ String getPluginName();
+ String getExportName();
+ Provider<T> getProvider();
+ }
+
static class NamePair {
private final String pluginName;
private final String exportName;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
index ec34887..b2f19e5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -26,6 +26,7 @@
import com.google.inject.util.Types;
import java.util.Collection;
+import java.util.Collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -127,6 +128,11 @@
return binder.bind(type).annotatedWith(name);
}
+ public static <T> DynamicSet<T> emptySet() {
+ return new DynamicSet<T>(
+ Collections.<AtomicReference<Provider<T>>> emptySet());
+ }
+
private final CopyOnWriteArrayList<AtomicReference<Provider<T>>> items;
DynamicSet(Collection<AtomicReference<Provider<T>>> base) {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
index 188011c..852c56e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -61,6 +61,8 @@
private String characterEncoding;
private long contentLength = -1;
private boolean gzip = true;
+ private boolean base64 = false;
+ private String attachmentName;
/** @return the MIME type of the result, for HTTP clients. */
public String getContentType() {
@@ -88,6 +90,17 @@
return this;
}
+ /** Get the attachment file name; null if not set. */
+ public String getAttachmentName() {
+ return attachmentName;
+ }
+
+ /** Set the attachment file name and return {@code this}. */
+ public BinaryResult setAttachmentName(String attachmentName) {
+ this.attachmentName = attachmentName;
+ return this;
+ }
+
/** @return length in bytes of the result; -1 if not known. */
public long getContentLength() {
return contentLength;
@@ -110,6 +123,17 @@
return this;
}
+ /** @return true if the result must be base64 encoded. */
+ public boolean isBase64() {
+ return base64;
+ }
+
+ /** Wrap the binary data in base64 encoding. */
+ public BinaryResult base64() {
+ base64 = true;
+ return this;
+ }
+
/**
* Write or copy the result onto the specified output stream.
*
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/CacheControl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/CacheControl.java
new file mode 100644
index 0000000..72f1bc5
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/CacheControl.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+import java.util.concurrent.TimeUnit;
+
+public class CacheControl {
+
+ public enum Type {
+ NONE, PUBLIC, PRIVATE;
+ }
+
+ public static final CacheControl NONE = new CacheControl(Type.NONE, 0, null);
+
+ public static CacheControl PUBLIC(long age, TimeUnit unit) {
+ return new CacheControl(Type.PUBLIC, age, unit);
+ }
+
+ public static CacheControl PRIVATE(long age, TimeUnit unit) {
+ return new CacheControl(Type.PRIVATE, age, unit);
+ }
+
+ private final Type type;
+ private final long age;
+ private final TimeUnit unit;
+ private boolean mustRevalidate;
+
+ private CacheControl(Type type, long age, TimeUnit unit) {
+ this.type = type;
+ this.age = age;
+ this.unit = unit;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public long getAge() {
+ return age;
+ }
+
+ public TimeUnit getUnit() {
+ return unit;
+ }
+
+ public boolean isMustRevalidate() {
+ return mustRevalidate;
+ }
+
+ public CacheControl setMustRevalidate() {
+ mustRevalidate = true;
+ return this;
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java
index f987425..f0f7dea 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java
@@ -25,6 +25,11 @@
return new IdString(id);
}
+ /** Construct an identifier from an already decoded string. */
+ public static IdString fromDecoded(String id) {
+ return new IdString(Url.encode(id));
+ }
+
private final String urlEncoded;
private IdString(String s) {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MethodNotAllowedException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MethodNotAllowedException.java
index 8b0fdd3..61c6345 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MethodNotAllowedException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MethodNotAllowedException.java
@@ -17,4 +17,13 @@
/** Method is not acceptable on the resource (HTTP 405 Method Not Allowed). */
public class MethodNotAllowedException extends RestApiException {
private static final long serialVersionUID = 1L;
+
+ public MethodNotAllowedException() {
+ super();
+ }
+
+ /** @param msg error text for client describing why the method is not allowed. */
+ public MethodNotAllowedException(String msg) {
+ super(msg);
+ }
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/PutInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/PutInput.java
deleted file mode 100644
index b2d62b3..0000000
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/PutInput.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.extensions.restapi;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-
-/** Raw data stream supplied by the body of a PUT. */
-public interface PutInput {
- String getContentType();
- long getContentLength();
- InputStream getInputStream() throws IOException;
-}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RawInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RawInput.java
new file mode 100644
index 0000000..4f195e4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RawInput.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Raw data stream supplied by the body of a PUT or POST. */
+public interface RawInput {
+ String getContentType();
+ long getContentLength();
+ InputStream getInputStream() throws IOException;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java
index eb6d811..aa503c1 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java
@@ -29,4 +29,9 @@
public ResourceConflictException(String msg) {
super(msg);
}
+
+ /** @param msg message to return to the client describing the error. */
+ public ResourceConflictException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
index aa891c9..76942e6 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
@@ -28,7 +28,18 @@
}
/** @param id portion of the resource URI that does not exist. */
+ public ResourceNotFoundException(String id, Throwable cause) {
+ super(id, cause);
+ }
+
+ /** @param id portion of the resource URI that does not exist. */
public ResourceNotFoundException(IdString id) {
super(id.get());
}
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public ResourceNotFoundException caching(CacheControl c) {
+ return super.caching(c);
+ }
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
index 97c4cbc..848004d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
@@ -50,11 +50,14 @@
public abstract int statusCode();
public abstract T value();
+ public abstract CacheControl caching();
+ public abstract Response<T> caching(CacheControl c);
public abstract String toString();
private static final class Impl<T> extends Response<T> {
private final int statusCode;
private final T value;
+ private CacheControl caching = CacheControl.NONE;
private Impl(int sc, T val) {
statusCode = sc;
@@ -72,6 +75,17 @@
}
@Override
+ public CacheControl caching() {
+ return caching;
+ }
+
+ @Override
+ public Response<T> caching(CacheControl c) {
+ caching = c;
+ return this;
+ }
+
+ @Override
public String toString() {
return "[" + statusCode() + "] " + value();
}
@@ -91,6 +105,16 @@
}
@Override
+ public CacheControl caching() {
+ return CacheControl.NONE;
+ }
+
+ @Override
+ public Response<Object> caching(CacheControl c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
public String toString() {
return "[204 No Content] None";
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java
index a6d27cd..3fae128 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java
@@ -17,6 +17,7 @@
/** Root exception type for JSON API failures. */
public abstract class RestApiException extends Exception {
private static final long serialVersionUID = 1L;
+ private CacheControl caching = CacheControl.NONE;
public RestApiException() {
}
@@ -28,4 +29,14 @@
public RestApiException(String msg, Throwable cause) {
super(msg, cause);
}
+
+ public CacheControl caching() {
+ return caching;
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T extends RestApiException> T caching(CacheControl c) {
+ caching = c;
+ return (T) this;
+ }
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
index 063a570..8032531 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
@@ -14,6 +14,8 @@
package com.google.gerrit.extensions.restapi;
+import java.sql.Timestamp;
+
/**
* Generic resource handle defining arguments to views.
* <p>
@@ -21,4 +23,18 @@
* {@link RestView} such as {@link RestReadView} or {@link RestModifyView}.
*/
public interface RestResource {
+
+ /** A resource with a last modification date. */
+ public interface HasLastModified {
+ /**
+ * @return time for the Last-Modified header. HTTP truncates the header
+ * value to seconds.
+ */
+ public Timestamp getLastModified();
+ }
+
+ /** A resource with an ETag. */
+ public interface HasETag {
+ public String getETag();
+ }
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java
deleted file mode 100644
index b2fb901..0000000
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.extensions.restapi;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-public interface StreamingResponse {
- public String getContentType();
- public void stream(OutputStream out) throws IOException;
-}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PrivateInternals_UiActionDescription.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PrivateInternals_UiActionDescription.java
new file mode 100644
index 0000000..6545db8
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/PrivateInternals_UiActionDescription.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.webui;
+
+/**
+ * Internal implementation helper for Gerrit Code Review server.
+ * <p>
+ * Extensions and plugins should not invoke this class.
+ */
+public class PrivateInternals_UiActionDescription {
+ public static void setMethod(UiAction.Description d, String method) {
+ d.setMethod(method);
+ }
+
+ public static void setId(UiAction.Description d, String id) {
+ d.setId(id);
+ }
+
+ private PrivateInternals_UiActionDescription() {
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
new file mode 100644
index 0000000..63172ce
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/UiAction.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.webui;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+
+public interface UiAction<R extends RestResource> extends RestView<R> {
+ /**
+ * Get the description of the action customized for the resource.
+ *
+ * @param resource the resource the view would act upon if the action is
+ * invoked by the client. Information from the resource can be used to
+ * customize the description.
+ * @return a description of the action. The server will populate the
+ * {@code id} and {@code method} properties. If null the action will
+ * assumed unavailable and not presented. This is usually the same as
+ * {@code setVisible(false)}.
+ */
+ public Description getDescription(R resource);
+
+ /** Describes an action invokable through the web interface. */
+ public static class Description {
+ private String method;
+ private String id;
+ private String label;
+ private String title;
+ private boolean visible = true;
+ private boolean enabled = true;
+
+ public String getMethod() {
+ return method;
+ }
+
+ /** {@code PrivateInternals_UiActionDescription.setMethod()} */
+ void setMethod(String method) {
+ this.method = method;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ /** {@code PrivateInternals_UiActionDescription.setId()} */
+ void setId(String id) {
+ this.id = id;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ /** Set the label to appear on the button to activate this action. */
+ public Description setLabel(String label) {
+ this.label = label;
+ return this;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ /** Set the tool-tip text to appear when the mouse hovers on the button. */
+ public Description setTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ public boolean isVisible() {
+ return visible;
+ }
+
+ /**
+ * Set if the action's button is visible on screen for the current client.
+ * If not visible the action description may not be sent to the client.
+ */
+ public Description setVisible(boolean visible) {
+ this.visible = visible;
+ return this;
+ }
+
+ public boolean isEnabled() {
+ return enabled && isVisible();
+ }
+
+ /** Set if the button should be invokable (true), or greyed out (false). */
+ public Description setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+ }
+}
diff --git a/gerrit-gwtdebug/BUCK b/gerrit-gwtdebug/BUCK
new file mode 100644
index 0000000..308c2f1
--- /dev/null
+++ b/gerrit-gwtdebug/BUCK
@@ -0,0 +1,6 @@
+java_library(
+ name = 'gwtdebug',
+ srcs = ['src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java'],
+ deps = ['//lib/gwt:dev'],
+ visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
deleted file mode 100644
index 83c10eb..0000000
--- a/gerrit-gwtdebug/pom.xml
+++ /dev/null
@@ -1,100 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-gwtdebug</artifactId>
- <name>Gerrit Code Review - GWT UI Debugging Support</name>
-
- <description>
- Debugging support for the GWT UI
- </description>
-
- <dependencies>
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-gwtui</artifactId>
- <version>${project.version}</version>
- <classifier>classes</classifier>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-war</artifactId>
- <version>${project.version}</version>
- <classifier>classes</classifier>
- <exclusions>
- <exclusion>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-pgm</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>postgresql</groupId>
- <artifactId>postgresql</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>bouncycastle</groupId>
- <artifactId>bcprov-jdk15</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>bouncycastle</groupId>
- <artifactId>bcpg-jdk15</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <!-- GWT should require these itself, but doesn't. -->
- <dependency>
- <groupId>javax.validation</groupId>
- <artifactId>validation-api</artifactId>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>javax.validation</groupId>
- <artifactId>validation-api</artifactId>
- <classifier>sources</classifier>
- <scope>provided</scope>
- </dependency>
-
- <!-- Workaround for overwriting our dependencies (like args4j) by additional
- classes put in gwt-dev.jar -->
- <dependency>
- <groupId>com.google.gwt</groupId>
- <artifactId>gwt-dev</artifactId>
- </dependency>
- </dependencies>
-</project>
diff --git a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
index d23aa35..a2a770e 100644
--- a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
+++ b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
@@ -381,9 +381,15 @@
Server server = new Server();
server.addConnector(connector);
- // warDir is "$top/gerrit-gwtui/target/gwt-hosted-mode"
- //
- File top = warDir.getParentFile().getParentFile().getParentFile();
+ File top;
+ String root = System.getProperty("gerrit.source_root");
+ if (root != null) {
+ top = new File(root);
+ } else {
+ // Under Maven warDir is "$top/gerrit-gwtui/target/gwt-hosted-mode"
+ top = warDir.getParentFile().getParentFile().getParentFile();
+ }
+
File app = new File(top, "gerrit-war/src/main/webapp");
File webxml = new File(app, "WEB-INF/web.xml");
diff --git a/gerrit-gwtexpui/BUCK b/gerrit-gwtexpui/BUCK
new file mode 100644
index 0000000..105ee7d
--- /dev/null
+++ b/gerrit-gwtexpui/BUCK
@@ -0,0 +1,104 @@
+SRC = 'src/main/java/com/google/gwtexpui/'
+
+gwt_module(
+ name = 'Clippy',
+ srcs = glob([SRC + 'clippy/client/*.java']),
+ gwtxml = SRC + 'clippy/Clippy.gwt.xml',
+ resources = [
+ SRC + 'clippy/client/clippy.css',
+ SRC + 'clippy/client/clippy.swf',
+ ],
+ deps = [
+ ':SafeHtml',
+ ':UserAgent',
+ '//lib/gwt:user',
+ '//lib:LICENSE-clippy',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+gwt_module(
+ name = 'CSS',
+ srcs = glob([SRC + 'css/rebind/*.java']),
+ gwtxml = SRC + 'css/CSS.gwt.xml',
+ deps = ['//lib/gwt:dev'],
+ visibility = ['PUBLIC'],
+)
+
+gwt_module(
+ name = 'GlobalKey',
+ srcs = glob([SRC + 'globalkey/client/*.java']),
+ gwtxml = SRC + 'globalkey/GlobalKey.gwt.xml',
+ resources = [
+ SRC + 'globalkey/client/KeyConstants.properties',
+ SRC + 'globalkey/client/key.css',
+ ],
+ deps = [
+ ':SafeHtml',
+ ':UserAgent',
+ '//lib/gwt:user',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+gwt_module(
+ name = 'Linker',
+ srcs = glob([SRC + 'linker/rebind/*.java']),
+ gwtxml = SRC + 'linker/ServerPlannedIFrameLinker.gwt.xml',
+ deps = ['//lib/gwt:dev'],
+ visibility = ['PUBLIC'],
+)
+
+java_library2(
+ name = 'linker_server',
+ srcs = glob([SRC + 'linker/server/*.java']),
+ compile_deps = ['//lib:servlet-api-3_0'],
+ visibility = ['PUBLIC'],
+)
+
+gwt_module(
+ name = 'Progress',
+ srcs = glob([SRC + 'progress/client/*.java']),
+ gwtxml = SRC + 'progress/Progress.gwt.xml',
+ resources = [SRC + 'progress/client/progress.css'],
+ deps = ['//lib/gwt:user'],
+ visibility = ['PUBLIC'],
+)
+
+gwt_module(
+ name = 'SafeHtml',
+ srcs = glob([SRC + 'safehtml/client/*.java']),
+ gwtxml = SRC + 'safehtml/SafeHtml.gwt.xml',
+ resources = [SRC + 'safehtml/client/safehtml.css'],
+ deps = ['//lib/gwt:user'],
+ visibility = ['PUBLIC'],
+)
+
+java_test(
+ name = 'SafeHtml_tests',
+ srcs = glob([
+ 'src/test/java/com/google/gwtexpui/safehtml/client/**/*.java',
+ ]),
+ deps = [
+ ':SafeHtml',
+ '//lib:junit',
+ '//lib/gwt:user',
+ '//lib/gwt:dev',
+ ],
+ source_under_test = [':SafeHtml'],
+)
+
+gwt_module(
+ name = 'UserAgent',
+ srcs = glob([SRC + 'user/client/*.java']),
+ gwtxml = SRC + 'user/User.gwt.xml',
+ deps = ['//lib/gwt:user'],
+ visibility = ['PUBLIC'],
+)
+
+java_library2(
+ name = 'server',
+ srcs = glob([SRC + 'server/*.java']),
+ compile_deps = ['//lib:servlet-api-3_0'],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-gwtexpui/pom.xml b/gerrit-gwtexpui/pom.xml
deleted file mode 100644
index 2c5ec62..0000000
--- a/gerrit-gwtexpui/pom.xml
+++ /dev/null
@@ -1,75 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-gwtexpui</artifactId>
-
- <name>Gerrit Code Review - GWT expui</name>
- <description>Extended UI tools for GWT</description>
-
- <build>
- <plugins>
- <plugin>
- <artifactId>maven-source-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>jar</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
-
- <extensions>
- <extension>
- <groupId>com.googlesource.gerrit</groupId>
- <artifactId>gs-maven-wagon</artifactId>
- <version>3.3</version>
- </extension>
- </extensions>
- </build>
-
- <dependencies>
- <dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <scope>test</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.gwt</groupId>
- <artifactId>gwt-user</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.gwt</groupId>
- <artifactId>gwt-dev</artifactId>
- <scope>provided</scope>
- </dependency>
- </dependencies>
-</project>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
index 4c2b8981..dfa7679 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyResources.java
@@ -16,10 +16,16 @@
import com.google.gwt.core.client.GWT;
import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.resources.client.DataResource.DoNotEmbed;
public interface ClippyResources extends ClientBundle {
public static final ClippyResources I = GWT.create(ClippyResources.class);
@Source("clippy.css")
ClippyCss css();
+
+ @Source("clippy.swf")
+ @DoNotEmbed
+ DataResource swf();
}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
index 273318b..8ec23a3 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
@@ -14,7 +14,9 @@
package com.google.gwtexpui.clippy.client;
-import com.google.gwt.core.client.GWT;
+import static com.google.gwt.dom.client.Style.Visibility.HIDDEN;
+import static com.google.gwt.dom.client.Style.Visibility.VISIBLE;
+
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
@@ -22,6 +24,7 @@
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.http.client.URL;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
@@ -34,6 +37,8 @@
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtexpui.user.client.DialogVisibleEvent;
+import com.google.gwtexpui.user.client.DialogVisibleHandler;
import com.google.gwtexpui.user.client.UserAgent;
/**
@@ -47,7 +52,6 @@
public class CopyableLabel extends Composite implements HasText {
private static final int SWF_WIDTH = 110;
private static final int SWF_HEIGHT = 14;
- private static String swfUrl;
private static boolean flashEnabled = true;
static {
@@ -63,10 +67,7 @@
}
private static String swfUrl() {
- if (swfUrl == null) {
- swfUrl = GWT.getModuleBaseURL() + "gwtexpui_clippy1.cache.swf";
- }
- return swfUrl;
+ return ClippyResources.I.swf().getSafeUri().asString();
}
private final FlowPanel content;
@@ -75,6 +76,11 @@
private Label textLabel;
private TextBox textBox;
private Element swf;
+ private HandlerRegistration hideHandler;
+
+ public CopyableLabel() {
+ this("");
+ }
/**
* Create a new label
@@ -122,12 +128,11 @@
public void setPreviewText(final String text) {
if (textLabel != null) {
textLabel.setText(text);
- visibleLen = text.length();
}
}
private void embedMovie() {
- if (flashEnabled && UserAgent.hasFlash) {
+ if (flashEnabled && UserAgent.hasFlash && text.length() > 0) {
final String flashVars = "text=" + URL.encodeQueryString(getText());
final SafeHtmlBuilder h = new SafeHtmlBuilder();
@@ -157,6 +162,27 @@
DOM.removeChild(getElement(), swf);
}
DOM.appendChild(getElement(), swf = SafeHtml.parse(h));
+ initHideHandler();
+ }
+ }
+
+ private void initHideHandler() {
+ if (hideHandler == null && swf != null && isAttached()) {
+ hideHandler =
+ UserAgent.addDialogVisibleHandler(new DialogVisibleHandler() {
+ @Override
+ public void onDialogVisible(DialogVisibleEvent event) {
+ if (event.contains(CopyableLabel.this)) {
+ if (event.isVisible()) {
+ swf.getStyle().setVisibility(VISIBLE);
+ }
+ } else {
+ swf.getStyle().setVisibility(event.isVisible()
+ ? HIDDEN
+ : VISIBLE);
+ }
+ }
+ });
}
}
@@ -178,11 +204,25 @@
embedMovie();
}
+ @Override
+ protected void onLoad() {
+ initHideHandler();
+ }
+
+ @Override
+ protected void onUnload() {
+ if (hideHandler != null) {
+ hideHandler.removeHandler();
+ hideHandler = null;
+ }
+ }
+
private void showTextBox() {
if (textBox == null) {
textBox = new TextBox();
textBox.setText(getText());
textBox.setVisibleLength(visibleLen);
+ textBox.setReadOnly(true);
textBox.addKeyPressHandler(new KeyPressHandler() {
@Override
public void onKeyPress(final KeyPressEvent event) {
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.swf
similarity index 100%
rename from gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf
rename to gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.swf
Binary files differ
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java
index d680a72..c59a4ea 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java
@@ -18,16 +18,20 @@
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.event.dom.client.HasMouseMoveHandlers;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.MouseMoveEvent;
+import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
-class DocWidget extends Widget implements HasKeyPressHandlers {
+public class DocWidget extends Widget
+ implements HasKeyPressHandlers, HasMouseMoveHandlers {
private static DocWidget me;
- static DocWidget get() {
+ public static DocWidget get() {
if (me == null) {
me = new DocWidget();
}
@@ -45,6 +49,11 @@
return addDomHandler(handler, KeyPressEvent.getType());
}
+ @Override
+ public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) {
+ return addDomHandler(handler, MouseMoveEvent.getType());
+ }
+
private static Node docnode() {
return Document.get();
}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
index 1eaaa3c..daf5d61 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
@@ -15,6 +15,8 @@
package com.google.gwtexpui.globalkey.client;
import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
@@ -92,6 +94,14 @@
active = new State(panel);
active.add(new HidePopupPanelCommand(0, KeyCodes.KEY_ESCAPE, panel));
panel.addCloseHandler(restoreGlobal);
+ panel.addDomHandler(new KeyDownHandler() {
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
+ panel.hide();
+ }
+ }
+ }, KeyDownEvent.getType());
}
public static HandlerRegistration addApplication(final Widget widget,
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
index 4f3205a..ad4c23e 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
@@ -121,7 +121,10 @@
}
static int toMask(final KeyPressEvent event) {
- int mask = event.getCharCode();
+ int mask = event.getUnicodeCharCode();
+ if (mask == 0) {
+ mask = event.getNativeEvent().getKeyCode();
+ }
if (event.isAltKeyDown()) {
mask |= KeyCommand.M_ALT;
}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
index 7bd0233..49c1b01 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
@@ -16,8 +16,11 @@
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.FlowPanel;
@@ -38,7 +41,7 @@
public class KeyHelpPopup extends PluginSafePopupPanel implements
- KeyPressHandler {
+ KeyPressHandler, KeyDownHandler {
private final FocusPanel focus;
public KeyHelpPopup() {
@@ -77,6 +80,7 @@
DOM.setStyleAttribute(focus.getElement(), "outline", "0px");
DOM.setElementAttribute(focus.getElement(), "hideFocus", "true");
focus.addKeyPressHandler(this);
+ focus.addKeyDownHandler(this);
add(focus);
}
@@ -100,6 +104,13 @@
hide();
}
+ @Override
+ public void onKeyDown(final KeyDownEvent event) {
+ if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
+ hide();
+ }
+ }
+
private void populate(final Grid lists) {
int end[] = new int[5];
int column = 0;
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpFlowPanel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpFlowPanel.java
new file mode 100644
index 0000000..7ae7fd0
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpFlowPanel.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.user.client.ui.FlowPanel;
+
+public class NpFlowPanel extends FlowPanel {
+ public NpFlowPanel() {
+ addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
index e2c576b..61bec18 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
@@ -79,7 +79,18 @@
if (!html) {
ds = escape(ds);
}
- displayString = sgi(ds, qstr, "<strong>$1</strong>");
+
+ // We now surround qstr by <strong>. But the chosen approach is not too
+ // smooth, if qstr is small (e.g.: "t") and this small qstr may occur in
+ // escapes (e.g.: "Tim <email@example.org>"). Those escapes will
+ // get <strong>-ed as well (e.g.: "<" -> "&<strong>l</strong>t;"). But
+ // as repairing those mangled escapes is easier than not mangling them in
+ // the first place, we repair them afterwards.
+ ds = sgi(ds, qstr, "<strong>$1</strong>");
+ // Repairing <strong>-ed escapes.
+ ds = sgi(ds, "(&[a-z]*)<strong>([a-z]*)</strong>([a-z]*;)", "$1$2$3");
+
+ displayString = ds;
}
private static native String sgi(String inString, String pat, String newHtml)
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
index 0a9f7a2..f752780 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
@@ -15,10 +15,10 @@
package com.google.gwtexpui.safehtml.client;
import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HTMLTable;
import com.google.gwt.user.client.ui.HasHTML;
@@ -29,7 +29,9 @@
import java.util.List;
/** Immutable string safely placed as HTML without further escaping. */
-public abstract class SafeHtml {
+@SuppressWarnings("serial")
+public abstract class SafeHtml
+ implements com.google.gwt.safehtml.shared.SafeHtml {
public static final SafeHtmlResources RESOURCES;
static {
@@ -84,13 +86,13 @@
}
/** @return the existing inner HTML of any element. */
- public static SafeHtml get(final Element e) {
- return new SafeHtmlString(DOM.getInnerHTML(e));
+ public static SafeHtml get(Element e) {
+ return new SafeHtmlString(e.getInnerHTML());
}
/** Set the inner HTML of any element. */
- public static Element set(final Element e, final SafeHtml str) {
- DOM.setInnerHTML(e, str.asString());
+ public static Element setInnerHTML(Element e, SafeHtml str) {
+ e.setInnerHTML(str.asString());
return e;
}
@@ -107,8 +109,10 @@
}
/** Parse an HTML block and return the first (typically root) element. */
- public static Element parse(final SafeHtml str) {
- return DOM.getFirstChild(set(DOM.createDiv(), str));
+ public static com.google.gwt.user.client.Element parse(SafeHtml html) {
+ com.google.gwt.user.client.Element e = DOM.createDiv();
+ setInnerHTML(e, html);
+ return DOM.getFirstChild(e);
}
/** Convert bare http:// and https:// URLs into <a href> tags. */
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
index 9fe3267..8ff99ee 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
@@ -19,6 +19,7 @@
/**
* Safely constructs a {@link SafeHtml}, escaping user provided content.
*/
+@SuppressWarnings("serial")
public class SafeHtmlBuilder extends SafeHtml {
private static final Impl impl;
@@ -317,6 +318,16 @@
return closeElement("td");
}
+ /** Append "<th>"; attributes may be set if needed */
+ public SafeHtmlBuilder openTh() {
+ return openElement("th");
+ }
+
+ /** Append "</th>" */
+ public SafeHtmlBuilder closeTh() {
+ return closeElement("th");
+ }
+
/** Append "<div>"; attributes may be set if needed */
public SafeHtmlBuilder openDiv() {
return openElement("div");
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
index a229421..57392bf 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
@@ -14,6 +14,7 @@
package com.google.gwtexpui.safehtml.client;
+@SuppressWarnings("serial")
class SafeHtmlString extends SafeHtml {
private final String html;
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
index c4d681f..2033c62 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
@@ -72,17 +72,14 @@
private static boolean cacheForever(final String pathInfo,
final HttpServletRequest req) {
- if (pathInfo.endsWith(".cache.html")) {
- return true;
- } else if (pathInfo.endsWith(".cache.gif")) {
- return true;
- } else if (pathInfo.endsWith(".cache.png")) {
- return true;
- } else if (pathInfo.endsWith(".cache.css")) {
- return true;
- } else if (pathInfo.endsWith(".cache.jar")) {
- return true;
- } else if (pathInfo.endsWith(".cache.swf")) {
+ if (pathInfo.endsWith(".cache.html")
+ || pathInfo.endsWith(".cache.gif")
+ || pathInfo.endsWith(".cache.png")
+ || pathInfo.endsWith(".cache.css")
+ || pathInfo.endsWith(".cache.jar")
+ || pathInfo.endsWith(".cache.swf")
+ || pathInfo.endsWith(".cache.txt")
+ || pathInfo.endsWith(".cache.js")) {
return true;
} else if (pathInfo.endsWith(".nocache.js")) {
final String v = req.getParameter("content");
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
index 11409e8..d44405f 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheHeaders.java
@@ -59,10 +59,34 @@
public static void setCacheable(
HttpServletRequest req, HttpServletResponse res,
long age, TimeUnit unit) {
+ setCacheable(req, res, age, unit, false);
+ }
+
+ /**
+ * Permit caching the response for up to the age specified.
+ * <p>
+ * If the request is on a secure connection (e.g. SSL) private caching is
+ * used. This allows the user-agent to cache the response, but requests
+ * intermediate proxies to not cache. This may offer better protection for
+ * Set-Cookie headers.
+ * <p>
+ * If the request is on plaintext (insecure), public caching is used. This may
+ * allow an intermediate proxy to cache the response, including any Set-Cookie
+ * header that may have also been included.
+ *
+ * @param req current request.
+ * @param res response being returned.
+ * @param age how long the response can be cached.
+ * @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+ * @param mustRevalidate true if the client must validate the cached entity.
+ */
+ public static void setCacheable(
+ HttpServletRequest req, HttpServletResponse res,
+ long age, TimeUnit unit, boolean mustRevalidate) {
if (req.isSecure()) {
- setCacheablePrivate(res, age, unit);
+ setCacheablePrivate(res, age, unit, mustRevalidate);
} else {
- setCacheablePublic(res, age, unit);
+ setCacheablePublic(res, age, unit, mustRevalidate);
}
}
@@ -76,15 +100,16 @@
* @param res response being returned.
* @param age how long the response can be cached.
* @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+ * @param mustRevalidate true if the client must validate the cached entity.
*/
public static void setCacheablePublic(HttpServletResponse res,
- long age, TimeUnit unit) {
+ long age, TimeUnit unit, boolean mustRevalidate) {
long now = System.currentTimeMillis();
long sec = maxAgeSeconds(age, unit);
res.setDateHeader("Expires", now + SECONDS.toMillis(sec));
res.setDateHeader("Date", now);
- cache(res, "public", age, unit);
+ cache(res, "public", age, unit, mustRevalidate);
}
/**
@@ -93,20 +118,22 @@
* @param res response being returned.
* @param age how long the response can be cached.
* @param unit time unit for age, usually {@link TimeUnit#SECONDS}.
+ * @param mustRevalidate true if the client must validate the cached entity.
*/
public static void setCacheablePrivate(HttpServletResponse res,
- long age, TimeUnit unit) {
+ long age, TimeUnit unit, boolean mustRevalidate) {
long now = System.currentTimeMillis();
res.setDateHeader("Expires", now);
res.setDateHeader("Date", now);
- cache(res, "private", age, unit);
+ cache(res, "private", age, unit, mustRevalidate);
}
private static void cache(HttpServletResponse res,
- String type, long age, TimeUnit unit) {
+ String type, long age, TimeUnit unit, boolean revalidate) {
res.setHeader("Cache-Control", String.format(
- "%s, max-age=%d",
- type, maxAgeSeconds(age, unit)));
+ "%s, max-age=%d%s",
+ type, maxAgeSeconds(age, unit),
+ revalidate ? ", must-revalidate" : ""));
}
private static long maxAgeSeconds(long age, TimeUnit unit) {
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml
index c681d89..f4f8d51 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/User.gwt.xml
@@ -15,13 +15,4 @@
-->
<module>
<inherits name="com.google.gwt.user.User"/>
-
- <replace-with class="com.google.gwtexpui.user.client.PluginSafeDialogBoxImplAutoHide">
- <when-type-is class="com.google.gwtexpui.user.client.PluginSafeDialogBoxImpl" />
- <any>
- <when-property-is name="user.agent" value="safari"/>
- <when-property-is name="user.agent" value="gecko"/>
- <when-property-is name="user.agent" value="gecko1_8"/>
- </any>
- </replace-with>
</module>
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleEvent.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleEvent.java
new file mode 100644
index 0000000..80a940a
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleEvent.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.user.client;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.user.client.ui.Widget;
+
+public class DialogVisibleEvent extends GwtEvent<DialogVisibleHandler> {
+ private static Type<DialogVisibleHandler> TYPE;
+
+ public static Type<DialogVisibleHandler> getType() {
+ if (TYPE == null) {
+ TYPE = new Type<DialogVisibleHandler>();
+ }
+ return TYPE;
+ }
+
+ private final Widget parent;
+ private final boolean visible;
+
+ DialogVisibleEvent(Widget w, boolean visible) {
+ this.parent = w;
+ this.visible = visible;
+ }
+
+ public boolean contains(Widget c) {
+ for (; c != null; c = c.getParent()) {
+ if (c == parent) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean isVisible() {
+ return visible;
+ }
+
+ @Override
+ public Type<DialogVisibleHandler> getAssociatedType() {
+ return getType();
+ }
+
+ @Override
+ protected void dispatch(DialogVisibleHandler handler) {
+ handler.onDialogVisible(this);
+ }
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleHandler.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleHandler.java
new file mode 100644
index 0000000..d242db6
--- /dev/null
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/DialogVisibleHandler.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.user.client;
+
+import com.google.gwt.event.shared.EventHandler;
+
+public interface DialogVisibleHandler extends EventHandler {
+ public void onDialogVisible(DialogVisibleEvent event);
+}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java
index c6ab09a..80bfba1 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBox.java
@@ -14,7 +14,6 @@
package com.google.gwtexpui.user.client;
-import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.ui.DialogBox;
/**
@@ -30,9 +29,6 @@
* prior setting when the dialog is hidden.
* */
public class PluginSafeDialogBox extends DialogBox {
- private final PluginSafeDialogBoxImpl impl =
- GWT.create(PluginSafeDialogBoxImpl.class);
-
public PluginSafeDialogBox() {
this(false);
}
@@ -47,19 +43,19 @@
@Override
public void setVisible(final boolean show) {
- impl.visible(show);
+ UserAgent.fireDialogVisible(this, show);
super.setVisible(show);
}
@Override
public void show() {
- impl.visible(true);
+ UserAgent.fireDialogVisible(this, true);
super.show();
}
@Override
public void hide(final boolean autoClosed) {
- impl.visible(false);
+ UserAgent.fireDialogVisible(this, false);
super.hide(autoClosed);
}
}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java
deleted file mode 100644
index a32fc99..0000000
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImpl.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gwtexpui.user.client;
-
-class PluginSafeDialogBoxImpl {
- void visible(final boolean dialogVisible) {
- }
-}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java
deleted file mode 100644
index e32fe78..0000000
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafeDialogBoxImplAutoHide.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gwtexpui.user.client;
-
-import com.google.gwt.dom.client.Document;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NodeList;
-import com.google.gwt.user.client.ui.UIObject;
-
-import java.util.ArrayList;
-
-class PluginSafeDialogBoxImplAutoHide extends PluginSafeDialogBoxImpl {
- private boolean hidden;
- private ArrayList<HiddenElement> hiddenElements =
- new ArrayList<HiddenElement>();
-
- @Override
- void visible(final boolean dialogVisible) {
- if (dialogVisible) {
- hideAll();
- } else {
- showAll();
- }
- }
-
- private void hideAll() {
- if (!hidden) {
- hideSet(Document.get().getElementsByTagName("object"));
- hideSet(Document.get().getElementsByTagName("embed"));
- hideSet(Document.get().getElementsByTagName("applet"));
- hidden = true;
- }
- }
-
- private void hideSet(final NodeList<Element> all) {
- for (int i = 0; i < all.getLength(); i++) {
- final Element e = all.getItem(i);
- if (UIObject.isVisible(e)) {
- hiddenElements.add(new HiddenElement(e));
- }
- }
- }
-
- private void showAll() {
- if (hidden) {
- for (final HiddenElement e : hiddenElements) {
- e.restore();
- }
- hiddenElements.clear();
- hidden = false;
- }
- }
-
- private static class HiddenElement {
- private final Element element;
- private final String visibility;
-
- HiddenElement(final Element element) {
- this.element = element;
- this.visibility = getVisibility(element);
- setVisibility(element, "hidden");
- }
-
- void restore() {
- setVisibility(element, visibility);
- }
-
- private static native String getVisibility(Element elem)
- /*-{ return elem.style.visibility; }-*/;
-
- private static native void setVisibility(Element elem, String disp)
- /*-{ elem.style.visibility = disp; }-*/;
- }
-}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java
index 7d9c9fc..1ed8f99 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java
@@ -14,7 +14,6 @@
package com.google.gwtexpui.user.client;
-import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.ui.PopupPanel;
/**
@@ -30,9 +29,6 @@
* prior setting when the dialog is hidden.
* */
public class PluginSafePopupPanel extends PopupPanel {
- private final PluginSafeDialogBoxImpl impl =
- GWT.create(PluginSafeDialogBoxImpl.class);
-
public PluginSafePopupPanel() {
this(false);
}
@@ -47,19 +43,19 @@
@Override
public void setVisible(final boolean show) {
- impl.visible(show);
+ UserAgent.fireDialogVisible(this, show);
super.setVisible(show);
}
@Override
public void show() {
- impl.visible(true);
+ UserAgent.fireDialogVisible(this, true);
super.show();
}
@Override
public void hide(final boolean autoClosed) {
- impl.visible(false);
+ UserAgent.fireDialogVisible(this, false);
super.hide(autoClosed);
}
}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
index 02ba9ae..c654902 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
@@ -15,7 +15,11 @@
package com.google.gwtexpui.user.client;
import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.event.shared.SimpleEventBus;
import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Widget;
/**
* User agent feature tests we don't create permutations for.
@@ -29,6 +33,16 @@
public class UserAgent {
/** Does the browser have ShockwaveFlash plugin enabled? */
public static final boolean hasFlash = hasFlash();
+ private static final EventBus bus = new SimpleEventBus();
+
+ public static HandlerRegistration addDialogVisibleHandler(
+ DialogVisibleHandler handler) {
+ return bus.addHandler(DialogVisibleEvent.getType(), handler);
+ }
+
+ static void fireDialogVisible(Widget w, boolean visible) {
+ bus.fireEvent(new DialogVisibleEvent(w, visible));
+ }
private static native boolean hasFlash()
/*-{
diff --git a/gerrit-gwtui/BUCK b/gerrit-gwtui/BUCK
new file mode 100644
index 0000000..f3c8a25
--- /dev/null
+++ b/gerrit-gwtui/BUCK
@@ -0,0 +1,130 @@
+include_defs('//gerrit-gwtui/gwt.defs')
+
+genrule(
+ name = 'ui_optdbg',
+ cmd = 'cd $TMP;' +
+ 'unzip -q $SRCDIR/ui_dbg.zip;' +
+ 'mv' +
+ ' gerrit_ui/gerrit_ui.nocache.js' +
+ ' gerrit_ui/gerrit_dbg.nocache.js;' +
+ 'unzip -qo $SRCDIR/ui_opt.zip;' +
+ 'mkdir -p $(dirname $OUT);' +
+ 'zip -qr $OUT .',
+ srcs = [
+ genfile('ui_dbg.zip'),
+ genfile('ui_opt.zip'),
+ ],
+ deps = [
+ ':ui_dbg',
+ ':ui_opt',
+ ],
+ out = 'ui_optdbg.zip',
+ visibility = ['PUBLIC'],
+)
+
+gwt_application(
+ name = 'ui_opt',
+ module_target = MODULE,
+ compiler_opts = [
+ '-strict',
+ '-style', 'OBF',
+ '-optimize', '9',
+ '-XdisableClassMetadata',
+ '-XdisableCastChecking',
+ ],
+ deps = APP_DEPS,
+)
+
+gwt_application(
+ name = 'ui_dbg',
+ module_target = MODULE,
+ compiler_opts = DEBUG_OPTS + ['-strict'],
+ deps = APP_DEPS,
+ visibility = ['//:eclipse'],
+)
+
+gwt_user_agent_permutations(
+ name = 'ui',
+ module_name = 'gerrit_ui',
+ module_target = MODULE,
+ compiler_opts = DEBUG_OPTS + ['-draftCompile'],
+ browsers = BROWSERS,
+ deps = APP_DEPS,
+ visibility = ['//:'],
+)
+
+DIFFY = glob(['src/main/java/com/google/gerrit/client/diffy*.png'])
+
+gwt_module(
+ name = 'ui_module',
+ srcs = glob(['src/main/java/**/*.java']),
+ gwtxml = 'src/main/java/%s.gwt.xml' % MODULE.replace('.', '/'),
+ resources = glob(['src/main/java/**/*'], excludes = DIFFY),
+ deps = [
+ ':diffy_logo',
+ '//gerrit-gwtexpui:Clippy',
+ '//gerrit-gwtexpui:CSS',
+ '//gerrit-gwtexpui:GlobalKey',
+ '//gerrit-gwtexpui:Linker',
+ '//gerrit-gwtexpui:Progress',
+ '//gerrit-gwtexpui:SafeHtml',
+ '//gerrit-gwtexpui:UserAgent',
+ '//gerrit-common:client',
+ '//gerrit-patch-jgit:client',
+ '//gerrit-prettify:client',
+ '//gerrit-reviewdb:client',
+ '//lib:gwtjsonrpc',
+ '//lib:gwtjsonrpc_src',
+ '//lib:gwtorm',
+ '//lib:jsr305',
+ '//lib/codemirror:codemirror',
+ '//lib/gwt:user',
+ '//lib/jgit:jgit',
+ ],
+ visibility = [
+ '//tools/eclipse:classpath',
+ '//Documentation:licenses.txt',
+ ],
+)
+
+prebuilt_jar(
+ name = 'diffy_logo',
+ binary_jar = genfile('diffy_images.jar'),
+ deps = [
+ '//lib:LICENSE-diffy',
+ '//lib:LICENSE-CC-BY3.0',
+ ':diffy_image_files_ln',
+ ],
+)
+
+genrule(
+ name = 'diffy_image_files_ln',
+ cmd = 'ln -s $(location :diffy_image_files) $OUT',
+ deps = [':diffy_image_files'],
+ out = 'diffy_images.jar',
+)
+
+java_library(
+ name = 'diffy_image_files',
+ resources = DIFFY,
+)
+
+java_test(
+ name = 'ui_tests',
+ srcs = glob(['src/test/java/**/*.java']),
+ resources = glob(['src/test/resources/**/*']) + [
+ 'src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml',
+ ],
+ deps = [
+ ':ui_module',
+ '//gerrit-common:client',
+ '//lib:junit',
+ '//lib/gwt:dev',
+ '//lib/gwt:user',
+ '//lib/gwt:gwt-test-utils',
+ '//lib/jgit:jgit',
+ ],
+ source_under_test = [':ui_module'],
+ vm_args = ['-Xmx512m'],
+ visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/gerrit-gwtui/gwt.defs b/gerrit-gwtui/gwt.defs
new file mode 100644
index 0000000..e407854
--- /dev/null
+++ b/gerrit-gwtui/gwt.defs
@@ -0,0 +1,77 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+BROWSERS = [
+ 'chrome',
+ 'firefox',
+ 'gecko1_8',
+ 'safari',
+ 'msie', 'ie6', 'ie8', 'ie9',
+]
+ALIASES = {
+ 'chrome': 'safari',
+ 'firefox': 'gecko1_8',
+ 'msie': 'ie9',
+}
+MODULE = 'com.google.gerrit.GerritGwtUI'
+
+DEBUG_OPTS = [
+ '-style', 'PRETTY',
+ '-optimize', '0',
+]
+
+APP_DEPS = [':ui_module']
+
+def gwt_user_agent_permutations(
+ name,
+ module_name,
+ module_target,
+ compiler_opts = [],
+ deps = [],
+ browsers = [],
+ visibility = []):
+ for ua in browsers:
+ impl = ua
+ if ua in ALIASES:
+ impl = ALIASES[ua]
+ xml = ''.join([
+ "<module rename-to='%s'>" % module_name,
+ "<inherits name='%s'/>" % module_target,
+ "<set-property name='user.agent' value='%s'/>" % impl,
+ "<set-property name='locale' value='default'/>",
+ "</module>",
+ ])
+ gwt = '%s_%s.gwt.xml' % (module_target.replace('.', '/'), ua)
+ jar = '%s_%s.gwtxml.jar' % (name, ua)
+ genrule(
+ name = '%s_%s_gwtxml_gen' % (name, ua),
+ cmd = 'cd $TMP;' +
+ ('mkdir -p $(dirname %s);' % gwt) +
+ ('echo "%s">%s;' % (xml, gwt)) +
+ 'zip -qr $OUT .',
+ out = jar,
+ )
+ prebuilt_jar(
+ name = '%s_%s_gwtxml_lib' % (name, ua),
+ binary_jar = genfile(jar),
+ deps = [':%s_%s_gwtxml_gen' % (name, ua)],
+ )
+ gwt_application(
+ name = '%s_%s' % (name, ua),
+ module_target = module_target + '_' + ua,
+ compiler_opts = compiler_opts,
+ deps = deps + [':%s_%s_gwtxml_lib' % (name, ua)],
+ visibility = visibility,
+ )
+
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
deleted file mode 100644
index 17065fe..0000000
--- a/gerrit-gwtui/pom.xml
+++ /dev/null
@@ -1,249 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-gwtui</artifactId>
- <name>Gerrit Code Review - GWT UI</name>
- <packaging>war</packaging>
-
- <description>
- Web interface built on top of Google Web Toolkit
- </description>
-
- <dependencies>
- <dependency>
- <groupId>com.google.gwt</groupId>
- <artifactId>gwt-user</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-gwtexpui</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-gwtexpui</artifactId>
- <version>${project.version}</version>
- <classifier>sources</classifier>
- <type>jar</type>
- </dependency>
-
- <dependency>
- <groupId>gwtjsonrpc</groupId>
- <artifactId>gwtjsonrpc</artifactId>
- </dependency>
- <dependency>
- <groupId>gwtjsonrpc</groupId>
- <artifactId>gwtjsonrpc</artifactId>
- <classifier>sources</classifier>
- <type>jar</type>
- </dependency>
-
- <dependency>
- <groupId>gwtorm</groupId>
- <artifactId>gwtorm</artifactId>
- </dependency>
- <dependency>
- <groupId>gwtorm</groupId>
- <artifactId>gwtorm</artifactId>
- <classifier>sources</classifier>
- <type>jar</type>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-reviewdb</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-reviewdb</artifactId>
- <version>${project.version}</version>
- <classifier>sources</classifier>
- <type>jar</type>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-common</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-common</artifactId>
- <version>${project.version}</version>
- <classifier>sources</classifier>
- <type>jar</type>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-patch-jgit</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-patch-jgit</artifactId>
- <version>${project.version}</version>
- <classifier>sources</classifier>
- <type>jar</type>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit</artifactId>
- </dependency>
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit</artifactId>
- <classifier>sources</classifier>
- <type>jar</type>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-prettify</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-prettify</artifactId>
- <version>${project.version}</version>
- <classifier>sources</classifier>
- <type>jar</type>
- </dependency>
-
- <!-- GWT should require these itself, but doesn't. -->
- <dependency>
- <groupId>javax.validation</groupId>
- <artifactId>validation-api</artifactId>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>javax.validation</groupId>
- <artifactId>validation-api</artifactId>
- <classifier>sources</classifier>
- <scope>provided</scope>
- </dependency>
- </dependencies>
-
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>gwt-maven-plugin</artifactId>
- <executions>
- <execution>
- <id>optimized</id>
- <configuration>
- <module>com.google.gerrit.GerritGwtUI</module>
- <extraJvmArgs>-Xmx512m</extraJvmArgs>
- <compileReport>${gwt.compileReport}</compileReport>
- <disableClassMetadata>true</disableClassMetadata>
- <disableCastChecking>true</disableCastChecking>
- </configuration>
- <goals>
- <goal>compile</goal>
- </goals>
- </execution>
- <execution>
- <id>debug</id>
- <configuration>
- <style>PRETTY</style>
- <module>com.google.gerrit.GerritGwtUI</module>
- <extraJvmArgs>-Xmx512m</extraJvmArgs>
- <disableClassMetadata>true</disableClassMetadata>
- <disableRunAsync>true</disableRunAsync>
- <webappDirectory>${project.build.directory}/${project.build.finalName}_dbg</webappDirectory>
- </configuration>
- <goals>
- <goal>compile</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-antrun-plugin</artifactId>
- <executions>
- <execution>
- <id>compress-html</id>
- <phase>prepare-package</phase>
- <goals>
- <goal>run</goal>
- </goals>
- <configuration>
- <target>
- <property name="dst" location="${project.build.directory}/${project.build.finalName}"/>
- <property name="dbg" location="${project.build.directory}/${project.build.finalName}_dbg"/>
- <property name="app" location="${dst}/gerrit_ui"/>
-
- <mkdir dir="${app}"/>
- <apply executable="gzip" addsourcefile="false">
- <arg value="-9"/>
- <fileset dir="${app}">
- <include name="**/*.html"/>
- <include name="**/*.css"/>
- <include name="deferredjs/**/*.js"/>
- </fileset>
- <redirector>
- <inputmapper type="glob" from="*" to="${app}/*"/>
- <outputmapper type="glob" from="*" to="${app}/*.gz"/>
- </redirector>
- </apply>
-
- <copy file="${dbg}/gerrit_ui/gerrit_ui.nocache.js"
- tofile="${app}/gerrit_dbg.nocache.js"/>
- <copy todir="${app}" overwrite="false">
- <fileset dir="${dbg}/gerrit_ui">
- <include name="**/*.html"/>
- <include name="**/*.css"/>
- <include name="deferredjs/**/*.js"/>
- </fileset>
- </copy>
- </target>
- </configuration>
- </execution>
- </executions>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-war-plugin</artifactId>
- <configuration>
- <packagingExcludes>WEB-INF/classes/**,WEB-INF/deploy/**,WEB-INF/lib/**</packagingExcludes>
- <attachClasses>true</attachClasses>
- <archive>
- <addMavenDescriptor>false</addMavenDescriptor>
- </archive>
- </configuration>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
index 5fdc5bb..d33a525 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
@@ -28,11 +28,15 @@
<inherits name='com.google.gerrit.Common'/>
<inherits name='com.google.gerrit.UserAgent'/>
<inherits name='org.eclipse.jgit.JGit'/>
+ <inherits name='net.codemirror.CodeMirror'/>
<extend-property name='locale' values='en'/>
<set-property-fallback name='locale' value='en'/>
<set-property name='locale' value='en'/>
<set-configuration-property name='UiBinder.useSafeHtmlTemplates' value='true'/>
+ <set-property name='gwt.logging.logLevel' value='SEVERE'/>
+ <set-property name='gwt.logging.popupHandler' value='DISABLED'/>
+
<entry-point class='com.google.gerrit.client.Gerrit'/>
</module>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
index 1dce6df..9dde95c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/UserAgent.gwt.xml
@@ -14,6 +14,13 @@
limitations under the License.
-->
<module>
+ <replace-with class="com.google.gerrit.client.api.PluginName.PluginNameMoz">
+ <when-type-is class="com.google.gerrit.client.api.PluginName" />
+ <when-property-is name="compiler.stackMode" value="native" />
+ <when-property-is name="user.agent" value="safari" />
+ <when-property-is name="user.agent" value="gecko1_8" />
+ </replace-with>
+
<replace-with class="com.google.gerrit.client.ui.FancyFlexTableImplIE6">
<when-type-is class="com.google.gerrit.client.ui.FancyFlexTableImpl" />
<any>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
index 5dcccb0..3197e48 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
@@ -15,6 +15,7 @@
package com.google.gerrit.client;
import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.account.AccountInfo.AvatarInfo;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gwt.event.dom.client.LoadEvent;
@@ -27,14 +28,15 @@
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.UIObject;
-public class AvatarImage extends Image {
-
+public class AvatarImage extends Image implements LoadHandler {
public AvatarImage() {
+ setVisible(false);
+ addLoadHandler(this);
}
/** A default sized avatar image. */
public AvatarImage(AccountInfo account) {
- this(account, 0);
+ this(account, 26, true);
}
/**
@@ -60,26 +62,46 @@
* avatar image
*/
public AvatarImage(AccountInfo account, int size, boolean addPopup) {
+ addLoadHandler(this);
setAccount(account, size, addPopup);
}
public void setAccount(AccountInfo account, int size, boolean addPopup) {
- setUrl(isGerritServer(account) ? getGerritServerAvatarUrl() :
- url(account.email(), size));
- setVisible(false);
-
- if (size > 0) {
- // If the provider does not resize the image, force it in the browser.
- setSize(size + "px", size + "px");
- }
-
- addLoadHandler(new LoadHandler() {
- @Override
- public void onLoad(LoadEvent event) {
- setVisible(true);
+ if (account == null) {
+ setVisible(false);
+ } else if (isGerritServer(account)) {
+ setVisible(true);
+ setResource(Gerrit.RESOURCES.gerritAvatar26());
+ } else if (account.has_avatar_info()) {
+ setVisible(false);
+ AvatarInfo info = account.avatar(size);
+ if (info != null) {
+ setWidth(info.width() > 0 ? info.width() + "px" : "");
+ setHeight(info.height() > 0 ? info.height() + "px" : "");
+ setUrl(info.url());
+ popup(account, addPopup);
}
- });
+ } else if (account.email() != null) {
+ // TODO Kill /accounts/*/avatar URL.
+ String u = account.email();
+ if (Gerrit.isSignedIn()
+ && u.equals(Gerrit.getUserAccount().getPreferredEmail())) {
+ u = "self";
+ }
+ RestApi api = new RestApi("/accounts/").id(u).view("avatar");
+ if (size > 0) {
+ api.addParameter("s", size);
+ setSize("", size + "px");
+ }
+ setVisible(false);
+ setUrl(api.url());
+ popup(account, addPopup);
+ } else {
+ setVisible(false);
+ }
+ }
+ private void popup(AccountInfo account, boolean addPopup) {
if (addPopup) {
PopupHandler popupHandler = new PopupHandler(account, this);
addMouseOverHandler(popupHandler);
@@ -87,33 +109,17 @@
}
}
+ @Override
+ public void onLoad(LoadEvent event) {
+ setVisible(true);
+ }
+
private static boolean isGerritServer(AccountInfo account) {
return account._account_id() == 0
&& Util.C.messageNoAuthor().equals(account.name());
}
- private static String getGerritServerAvatarUrl() {
- return Gerrit.RESOURCES.gerritAvatar().getSafeUri().asString();
- }
-
- private static String url(String email, int size) {
- if (email == null) {
- return "";
- }
- String u;
- if (Gerrit.isSignedIn() && email.equals(Gerrit.getUserAccount().getPreferredEmail())) {
- u = "self";
- } else {
- u = email;
- }
- RestApi api = new RestApi("/accounts/").id(u).view("avatar");
- if (size > 0) {
- api.addParameter("s", size);
- }
- return api.url();
- }
-
- private class PopupHandler implements MouseOverHandler, MouseOutHandler {
+ private static class PopupHandler implements MouseOverHandler, MouseOutHandler {
private final AccountInfo account;
private final UIObject target;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.java
index e3b7525..8fef111 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationCallback.java
@@ -18,12 +18,18 @@
* Interface that a caller must implement to react on the result of a
* {@link ConfirmationDialog}.
*/
-public interface ConfirmationCallback {
+public abstract class ConfirmationCallback {
/**
* Called when the {@link ConfirmationDialog} is finished with OK.
* To be overwritten by subclasses.
*/
- public void onOk();
+ public abstract void onOk();
+ /**
+ * Called when the {@link ConfirmationDialog} is finished with Cancel.
+ * To be overwritten by subclasses.
+ */
+ public void onCancel() {
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
index 36169db..d510f05 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
@@ -55,6 +55,7 @@
@Override
public void onClick(ClickEvent event) {
hide();
+ callback.onCancel();
}
});
buttons.add(cancelButton);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index 87a09cf..685c4b8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -60,6 +60,7 @@
import com.google.gerrit.client.admin.ProjectInfoScreen;
import com.google.gerrit.client.admin.ProjectListScreen;
import com.google.gerrit.client.admin.ProjectScreen;
+import com.google.gerrit.client.change.ChangeScreen2;
import com.google.gerrit.client.changes.AccountDashboardScreen;
import com.google.gerrit.client.changes.ChangeScreen;
import com.google.gerrit.client.changes.CustomDashboardScreen;
@@ -69,6 +70,7 @@
import com.google.gerrit.client.changes.QueryScreen;
import com.google.gerrit.client.dashboards.DashboardInfo;
import com.google.gerrit.client.dashboards.DashboardList;
+import com.google.gerrit.client.diff.SideBySide2;
import com.google.gerrit.client.groups.GroupApi;
import com.google.gerrit.client.groups.GroupInfo;
import com.google.gerrit.client.patches.PatchScreen;
@@ -78,6 +80,7 @@
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.PatchSetDetail;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Patch;
@@ -90,6 +93,8 @@
import com.google.gwtorm.client.KeyUtil;
public class Dispatcher {
+ private static boolean useChangeScreen2;
+
public static String toPatchSideBySide(final Patch.Key id) {
return toPatch("", null, id);
}
@@ -98,6 +103,11 @@
return toPatch("", diffBase, id);
}
+ public static String toPatchSideBySide2(PatchSet.Id diffBase,
+ PatchSet.Id revision, String fileName) {
+ return toPatch("cm", diffBase, revision, fileName);
+ }
+
public static String toPatchUnified(final Patch.Key id) {
return toPatch("unified", null, id);
}
@@ -107,14 +117,18 @@
}
private static String toPatch(String type, PatchSet.Id diffBase, Patch.Key id) {
- PatchSet.Id ps = id.getParentKey();
- Change.Id c = ps.getParentKey();
+ return toPatch(type, diffBase, id.getParentKey(), id.get());
+ }
+
+ private static String toPatch(String type, PatchSet.Id diffBase,
+ PatchSet.Id revision, String fileName) {
+ Change.Id c = revision.getParentKey();
StringBuilder p = new StringBuilder();
p.append("/c/").append(c).append("/");
if (diffBase != null) {
p.append(diffBase.get()).append("..");
}
- p.append(ps.get()).append("/").append(KeyUtil.encode(id.get()));
+ p.append(revision.get()).append("/").append(KeyUtil.encode(fileName));
if (type != null && !type.isEmpty()) {
p.append(",").append(type);
}
@@ -189,6 +203,9 @@
} else if (matchPrefix("/c/", token)) {
change(token);
+ } else if (matchPrefix("/c2/", token)) {
+ change2(token);
+
} else if (matchExact(MINE, token)) {
Gerrit.display(token, mine(token));
@@ -462,8 +479,10 @@
}
if (rest.isEmpty()) {
- Gerrit.display(token, panel== null //
- ? new ChangeScreen(id) //
+ Gerrit.display(token, panel== null
+ ? (useChangeScreen2
+ ? new ChangeScreen2(id, null, false)
+ : new ChangeScreen(id))
: new NotFoundScreen());
return;
}
@@ -494,7 +513,9 @@
patch(token, base, p, 0, null, null, panel);
} else {
if (panel == null) {
- Gerrit.display(token, new ChangeScreen(ps));
+ Gerrit.display(token, useChangeScreen2
+ ? new ChangeScreen2(id, String.valueOf(ps.get()), false)
+ : new ChangeScreen(id));
} else if ("publish".equals(panel)) {
publish(ps);
} else {
@@ -503,6 +524,21 @@
}
}
+ private static void change2(final String token) {
+ String rest = skip(token);
+ Change.Id id;
+ int s = rest.indexOf('/');
+ if (0 <= s) {
+ id = Change.Id.parse(rest.substring(0, s));
+ rest = rest.substring(s + 1);
+ } else {
+ id = Change.Id.parse(rest);
+ rest = "";
+ }
+ useChangeScreen2 = true;
+ Gerrit.display(token, new ChangeScreen2(id, rest, false));
+ }
+
private static void publish(final PatchSet.Id ps) {
String token = toPublish(ps);
new AsyncSplit(token) {
@@ -567,6 +603,20 @@
top, //
baseId //
);
+ } else if ("cm".equals(panel)) {
+ if (Gerrit.isSignedIn()
+ && DiffView.UNIFIED_DIFF.equals(Gerrit.getUserAccount()
+ .getGeneralPreferences().getDiffView())) {
+ return new PatchScreen.Unified( //
+ id, //
+ patchIndex, //
+ patchSetDetail, //
+ patchTable, //
+ top, //
+ baseId //
+ );
+ }
+ return new SideBySide2(baseId, id.getParentKey(), id.get());
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index 0b1af89..e4ffb22 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -21,6 +21,7 @@
import com.google.gerrit.client.account.AccountCapabilities;
import com.google.gerrit.client.account.AccountInfo;
import com.google.gerrit.client.admin.ProjectScreen;
+import com.google.gerrit.client.api.ApiGlue;
import com.google.gerrit.client.changes.ChangeConstants;
import com.google.gerrit.client.changes.ChangeListScreen;
import com.google.gerrit.client.patches.PatchScreen;
@@ -31,7 +32,6 @@
import com.google.gerrit.client.ui.PatchLink;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.client.ui.ScreenLoadEvent;
-import com.google.gerrit.common.ClientVersion;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.GerritConfig;
import com.google.gerrit.common.data.GitwebConfig;
@@ -73,7 +73,6 @@
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwt.user.client.ui.InlineHTML;
import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwtexpui.clippy.client.CopyableLabel;
import com.google.gwtexpui.user.client.UserAgent;
@@ -109,13 +108,16 @@
private static LinkMenuBar menuRight;
private static LinkMenuBar diffBar;
private static LinkMenuBar projectsBar;
+ private static RootPanel topMenu;
private static RootPanel siteHeader;
private static RootPanel siteFooter;
+ private static RootPanel bottomMenu;
private static SearchPanel searchPanel;
private static final Dispatcher dispatcher = new Dispatcher();
private static ViewSite<Screen> body;
private static PatchScreen patchScreen;
private static String lastChangeListToken;
+ private static String lastViewToken;
static {
SYSTEM_SVC = GWT.create(SystemInfoService.class);
@@ -144,6 +146,10 @@
}
}
+ public static String getPriorView() {
+ return lastViewToken;
+ }
+
/**
* Load the screen at the given location, displaying when ready.
* <p>
@@ -230,6 +236,35 @@
}
}
+ public static int getHeaderFooterHeight() {
+ int h = bottomMenu.getOffsetHeight();
+ if (topMenu.isVisible()) {
+ h += topMenu.getOffsetHeight();
+ }
+ if (siteHeader.isVisible()) {
+ h += siteHeader.getOffsetHeight();
+ }
+ if (siteFooter.isVisible()) {
+ h += siteFooter.getOffsetHeight();
+ }
+ return h;
+ }
+
+ public static void setHeaderVisible(boolean visible) {
+ topMenu.setVisible(visible);
+ siteHeader.setVisible(visible && (myAccount != null
+ ? myAccount.getGeneralPreferences().isShowSiteHeader()
+ : true));
+ }
+
+ public static boolean isHeaderVisible() {
+ return topMenu.isVisible();
+ }
+
+ public static RootPanel getBottomMenu() {
+ return bottomMenu;
+ }
+
/** Get the public configuration data used by this Gerrit instance. */
public static GerritConfig getConfig() {
return myConfig;
@@ -434,47 +469,36 @@
}
}
- private static void populateBottomMenu(final RootPanel btmmenu) {
- final Label keyHelp = new Label(C.keyHelp());
- keyHelp.setStyleName(RESOURCES.css().keyhelp());
- btmmenu.add(keyHelp);
-
- String vs;
- if (GWT.isScript()) {
- final ClientVersion v = GWT.create(ClientVersion.class);
- vs = v.version().getText();
- if (vs.startsWith("v")) {
- vs = vs.substring(1);
- }
- } else {
+ private static void populateBottomMenu(RootPanel btmmenu, HostPageData hpd) {
+ String vs = hpd.version;
+ if (vs == null || vs.isEmpty()) {
vs = "dev";
}
- FlowPanel poweredBy = new FlowPanel();
- poweredBy.setStyleName(RESOURCES.css().version());
- poweredBy.add(new InlineHTML(M.poweredBy(vs)));
+ btmmenu.add(new InlineLabel(C.keyHelp()));
+ btmmenu.add(new InlineLabel(" | "));
+ btmmenu.add(new InlineHTML(M.poweredBy(vs)));
if (getConfig().getReportBugUrl() != null) {
- poweredBy.add(new InlineLabel(" | "));
Anchor a = new Anchor(
C.reportBug(),
getConfig().getReportBugUrl());
a.setTarget("_blank");
a.setStyleName("");
- poweredBy.add(a);
+ btmmenu.add(new InlineLabel(" | "));
+ btmmenu.add(a);
}
- btmmenu.add(poweredBy);
}
private void onModuleLoad2(HostPageData hpd) {
RESOURCES.gwt_override().ensureInjected();
RESOURCES.css().ensureInjected();
- final RootPanel gTopMenu = RootPanel.get("gerrit_topmenu");
+ topMenu = RootPanel.get("gerrit_topmenu");
final RootPanel gStarting = RootPanel.get("gerrit_startinggerrit");
final RootPanel gBody = RootPanel.get("gerrit_body");
- final RootPanel gBottomMenu = RootPanel.get("gerrit_btmmenu");
+ bottomMenu = RootPanel.get("gerrit_btmmenu");
- gTopMenu.setStyleName(RESOURCES.css().gerritTopMenu());
+ topMenu.setStyleName(RESOURCES.css().gerritTopMenu());
gBody.setStyleName(RESOURCES.css().gerritBody());
final Grid menuLine = new Grid(1, 3);
@@ -483,7 +507,7 @@
searchPanel = new SearchPanel();
menuLeft.setStyleName(RESOURCES.css().topmenuMenuLeft());
menuLine.setStyleName(RESOURCES.css().topmenu());
- gTopMenu.add(menuLine);
+ topMenu.add(menuLine);
final FlowPanel menuRightPanel = new FlowPanel();
menuRightPanel.setStyleName(RESOURCES.css().topmenuMenuRight());
menuRightPanel.add(searchPanel);
@@ -502,8 +526,9 @@
body = new ViewSite<Screen>() {
@Override
protected void onShowView(Screen view) {
- final String token = view.getToken();
- if (!token.equals(History.getToken())) {
+ lastViewToken = History.getToken();
+ String token = view.getToken();
+ if (!token.equals(lastViewToken)) {
History.newItem(token, false);
dispatchHistoryHooks(token);
}
@@ -518,7 +543,7 @@
};
gBody.add(body);
- RpcStatus.INSTANCE = new RpcStatus(gTopMenu);
+ RpcStatus.INSTANCE = new RpcStatus(topMenu);
JsonUtil.addRpcStartHandler(RpcStatus.INSTANCE);
JsonUtil.addRpcCompleteHandler(RpcStatus.INSTANCE);
JsonUtil.setDefaultXsrfManager(new XsrfManager() {
@@ -539,7 +564,7 @@
applyUserPreferences();
initHistoryHooks();
- populateBottomMenu(gBottomMenu);
+ populateBottomMenu(bottomMenu, hpd);
refreshMenuBar();
History.addValueChangeHandler(new ValueChangeHandler<String>() {
@@ -571,7 +596,8 @@
}
private void loadPlugins(HostPageData hpd, final String token) {
- if (hpd.plugins != null) {
+ ApiGlue.init();
+ if (hpd.plugins != null && !hpd.plugins.isEmpty()) {
for (final String url : hpd.plugins) {
ScriptInjector.fromUrl(url)
.setWindow(ScriptInjector.TOP_WINDOW)
@@ -703,8 +729,6 @@
whoAmI(cfg.getAuthType() != AuthType.CLIENT_SSL_CERT_LDAP);
} else {
switch (cfg.getAuthType()) {
- case HTTP:
- case HTTP_LDAP:
case CLIENT_SSL_CERT_LDAP:
break;
@@ -733,6 +757,14 @@
});
break;
+ case HTTP:
+ case HTTP_LDAP:
+ if (cfg.getLoginUrl() != null) {
+ final String signinText = cfg.getLoginText() == null ? C.menuSignIn() : cfg.getLoginText();
+ menuRight.add(anchor(signinText, cfg.getLoginUrl()));
+ }
+ break;
+
case LDAP:
case LDAP_BIND:
case CUSTOM_EXTENSION:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index 683f058..fb72085 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -18,9 +18,7 @@
public interface GerritConstants extends Constants {
String menuSignIn();
- String menuSignOut();
String menuRegister();
- String menuSettings();
String reportBug();
String signInDialogTitle();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index defc7e4..6f3dca5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -1,7 +1,5 @@
menuSignIn = Sign In
-menuSignOut = Sign Out
menuRegister = Register
-menuSettings = Settings
reportBug = Report Bug
signInDialogTitle = Code Review - Sign In
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index a33556e..2fef7ee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -140,7 +140,6 @@
String infoBlock();
String infoTable();
String inputFieldTypeHint();
- String keyhelp();
String labelList();
String leftMostCell();
String lineHeader();
@@ -148,6 +147,7 @@
String link();
String linkMenuBar();
String linkMenuItemNotLast();
+ String maxObjectSizeLimitPanel();
String menuBarUserName();
String menuBarUserNameAvatar();
String menuBarUserNameFocusPanel();
@@ -185,6 +185,7 @@
String projectAdminLabelValue();
String projectFilterLabel();
String projectFilterPanel();
+ String projectNameColumn();
String publishCommentsScreen();
String registerScreenExplain();
String registerScreenNextLinks();
@@ -201,6 +202,7 @@
String screenHeader();
String screenNoHeader();
String searchPanel();
+ String suggestBoxPopup();
String sectionHeader();
String selectPatchSetOldVersion();
String sideBySideScreenLinkTable();
@@ -230,6 +232,5 @@
String userInfoPopup();
String useridentity();
String usernameField();
- String version();
String watchedProjectFilter();
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
index 098cc77..80d65b0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
@@ -52,8 +52,8 @@
@Source("addFileComment.png")
public ImageResource addFileComment();
- @Source("diffy.png")
- public ImageResource gerritAvatar();
+ @Source("diffy26.png")
+ public ImageResource gerritAvatar26();
@Source("draftComments.png")
public ImageResource draftComments();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
index ec97e58..493b3f8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
@@ -14,6 +14,7 @@
package com.google.gerrit.client;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
import com.google.gerrit.common.data.GitWebType;
import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.reviewdb.client.Branch;
@@ -45,19 +46,26 @@
return !ps.isDraft() || type.getLinkDrafts();
}
+ public boolean canLink(RevisionInfo revision) {
+ return revision.draft() || type.getLinkDrafts();
+ }
+
public String getLinkName() {
return "(" + type.getLinkName() + ")";
}
- public String toRevision(final Project.NameKey project, final PatchSet ps) {
+ public String toRevision(String project, String commit) {
ParameterizedString pattern = new ParameterizedString(type.getRevision());
-
- final Map<String, String> p = new HashMap<String, String>();
- p.put("project", encode(project.get()));
- p.put("commit", encode(ps.getRevision().get()));
+ Map<String, String> p = new HashMap<String, String>();
+ p.put("project", encode(project));
+ p.put("commit", encode(commit));
return baseUrl + pattern.replace(p);
}
+ public String toRevision(final Project.NameKey project, final PatchSet ps) {
+ return toRevision(project.get(), ps.getRevision().get());
+ }
+
public String toProject(final Project.NameKey project) {
ParameterizedString pattern = new ParameterizedString(type.getProject());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
index 3298a06..143c148 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
@@ -23,19 +23,19 @@
* in the format defined by {@code git log --relative-date}.
*/
public class RelativeDateFormatter {
- final static long SECOND_IN_MILLIS = 1000;
+ static final long SECOND_IN_MILLIS = 1000;
- final static long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
+ static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
- final static long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
+ static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
- final static long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
+ static final long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
- final static long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
+ static final long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
- final static long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
+ static final long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
- final static long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
+ static final long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
/**
* @param when {@link Date} to format
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
index 46b7d4d..f86e1fe 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
@@ -119,6 +119,12 @@
private static class MySuggestionDisplay extends SuggestBox.DefaultSuggestionDisplay {
private boolean isSuggestionSelected;
+ private MySuggestionDisplay() {
+ super();
+
+ getPopupPanel().addStyleName(Gerrit.RESOURCES.css().suggestBoxPopup());
+ }
+
@Override
protected Suggestion getCurrentSelection() {
Suggestion currentSelection = super.getCurrentSelection();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index dd4de33..da9f1c6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -79,6 +79,7 @@
suggestions.add("reviewerin:");
suggestions.add("commit:");
+ suggestions.add("comment:");
suggestions.add("project:");
suggestions.add("branch:");
suggestions.add("topic:");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
index a532209..ceb50a8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Themer.java
@@ -10,7 +10,7 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
-// limitations under the License.package com.google.gerrit.server.git;
+// limitations under the License.
package com.google.gerrit.client;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
index 01811a6..90348db 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
@@ -15,42 +15,34 @@
package com.google.gerrit.client;
import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.AnchorElement;
+import com.google.gwt.dom.client.Element;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.user.client.PluginSafePopupPanel;
public class UserPopupPanel extends PluginSafePopupPanel {
- interface Binder extends UiBinder<Widget, UserPopupPanel> {
- }
-
+ interface Binder extends UiBinder<Widget, UserPopupPanel> {}
private static final Binder binder = GWT.create(Binder.class);
- @UiField(provided = true)
- AvatarImage avatar;
- @UiField
- Label userName;
- @UiField
- Label userEmail;
- @UiField
- Anchor logout;
- @UiField
- Anchor settings;
+ @UiField(provided = true) AvatarImage avatar;
+ @UiField Label userName;
+ @UiField Label userEmail;
+ @UiField Element userLinks;
+ @UiField AnchorElement switchAccount;
+ @UiField AnchorElement logout;
+ @UiField InlineHyperlink settings;
public UserPopupPanel(AccountInfo account, boolean canLogOut,
boolean showSettingsLink) {
super(/* auto hide */true, /* modal */false);
avatar = new AvatarImage(account, 100, false);
setWidget(binder.createAndBindUi(this));
- // We must show and then hide this popup so that it is part of the DOM.
- // Otherwise the image does not get any events. Calling hide() would
- // remove it from the DOM so we use setVisible(false) instead.
- show();
- setVisible(false);
setStyleName(Gerrit.RESOURCES.css().userInfoPopup());
if (account.name() != null) {
userName.setText(account.name());
@@ -58,15 +50,35 @@
if (account.email() != null) {
userEmail.setText(account.email());
}
- if (canLogOut) {
- logout.setHref(Gerrit.selfRedirect("/logout"));
- } else {
- logout.setVisible(false);
- }
if (showSettingsLink) {
- settings.setHref(Gerrit.selfRedirect(PageLinks.SETTINGS));
+ if (Gerrit.getConfig().getSwitchAccountUrl() != null) {
+ switchAccount.setHref(Gerrit.getConfig().getSwitchAccountUrl());
+ } else if (Gerrit.getConfig().getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT
+ || Gerrit.getConfig().getAuthType() == AuthType.OPENID) {
+ switchAccount.setHref(Gerrit.selfRedirect("/login/"));
+ } else {
+ switchAccount.removeFromParent();
+ switchAccount = null;
+ }
+ if (canLogOut) {
+ logout.setHref(Gerrit.selfRedirect("/logout"));
+ } else {
+ logout.removeFromParent();
+ logout = null;
+ }
+
} else {
- settings.setVisible(false);
+ settings.removeFromParent();
+ settings = null;
+ userLinks.removeFromParent();
+ userLinks = null;
+ logout = null;
}
+
+ // We must show and then hide this popup so that it is part of the DOM.
+ // Otherwise the image does not get any events. Calling hide() would
+ // remove it from the DOM so we use setVisible(false) instead.
+ show();
+ setVisible(false);
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
index 0db5788..cd51485 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
@@ -17,9 +17,8 @@
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:g='urn:import:com.google.gwt.user.client.ui'
- xmlns:gerrit='urn:import:com.google.gerrit.client'>
- <ui:with field='constants' type='com.google.gerrit.client.GerritConstants'/>
-
+ xmlns:gerrit='urn:import:com.google.gerrit.client'
+ xmlns:u='urn:import:com.google.gerrit.client.ui'>
<ui:style>
.panel {
padding: 8px;
@@ -38,10 +37,17 @@
.email {
padding-bottom: 6px;
}
- .logout {
- padding-left: 16px;
+ .userLinks {
+ min-width: 250px;
+ }
+ .userLinksRight {
float: right;
}
+ .switchAccount {
+ border-right: 1px solid black;
+ padding-right: 0.5em;
+ margin-right: 0.5em;
+ }
</ui:style>
<g:HTMLPanel styleName='{style.panel}'>
@@ -51,11 +57,14 @@
<g:Label ui:field='userName' styleName="{style.userName}" />
<g:Label ui:field='userEmail' styleName="{style.email}" />
</td></tr></table>
- <g:Anchor ui:field='settings'>
- <ui:text from='{constants.menuSettings}' />
- </g:Anchor>
- <g:Anchor ui:field='logout' styleName="{style.logout}">
- <ui:text from='{constants.menuSignOut}' />
- </g:Anchor>
+ <div ui:field='userLinks' class='{style.userLinks}'>
+ <u:InlineHyperlink ui:field='settings' targetHistoryToken='/settings/'>
+ <ui:msg>Settings</ui:msg>
+ </u:InlineHyperlink>
+ <span class='{style.userLinksRight}'>
+ <a ui:field='switchAccount' class='{style.switchAccount}'><ui:msg>Switch Account</ui:msg></a
+ ><a ui:field='logout'><ui:msg>Sign Out</ui:msg></a>
+ </span>
+ </div>
</g:HTMLPanel>
-</ui:UiBinder>
\ No newline at end of file
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/AccessMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/AccessMap.java
new file mode 100644
index 0000000..629f725
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/AccessMap.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.access;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.Collections;
+import java.util.Set;
+
+/** Access rights available from {@code /access/}. */
+public class AccessMap extends NativeMap<ProjectAccessInfo> {
+ public static void get(Set<Project.NameKey> projects,
+ AsyncCallback<AccessMap> callback) {
+ RestApi api = new RestApi("/access/");
+ for (Project.NameKey p : projects) {
+ api.addParameter("project", p.get());
+ }
+ api.get(NativeMap.copyKeysIntoChildren(callback));
+ }
+
+ public static void get(final Project.NameKey project,
+ final AsyncCallback<ProjectAccessInfo> cb) {
+ get(Collections.singleton(project), new AsyncCallback<AccessMap>() {
+ @Override
+ public void onSuccess(AccessMap result) {
+ cb.onSuccess(result.get(project.get()));
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ cb.onFailure(caught);
+ }
+ });
+ }
+
+ protected AccessMap() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
new file mode 100644
index 0000000..7cfb1fc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/ProjectAccessInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.access;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ProjectAccessInfo extends JavaScriptObject {
+ public final native boolean canAddRefs() /*-{ return this.can_add ? true : false; }-*/;
+ public final native boolean isOwner() /*-{ return this.is_owner ? true : false; }-*/;
+
+ protected ProjectAccessInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
new file mode 100644
index 0000000..06fad36
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.account;
+
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.Set;
+
+/**
+ * A collection of static methods which work on the Gerrit REST API for specific
+ * accounts.
+ */
+public class AccountApi {
+ public static RestApi self() {
+ return new RestApi("/accounts/").view("self");
+ }
+
+ /** Retrieve the username */
+ public static void getUsername(String account, AsyncCallback<NativeString> cb) {
+ new RestApi("/accounts/").id(account).view("username").get(cb);
+ }
+
+ /** Retrieve email addresses */
+ public static void getEmails(String account,
+ AsyncCallback<JsArray<EmailInfo>> cb) {
+ new RestApi("/accounts/").id(account).view("emails").get(cb);
+ }
+
+ /** Register a new email address */
+ public static void registerEmail(String account, String email,
+ AsyncCallback<EmailInfo> cb) {
+ JavaScriptObject in = JavaScriptObject.createObject();
+ new RestApi("/accounts/").id(account).view("emails").id(email)
+ .ifNoneMatch().put(in, cb);
+ }
+
+ /** Retrieve SSH keys */
+ public static void getSshKeys(String account,
+ AsyncCallback<JsArray<SshKeyInfo>> cb) {
+ new RestApi("/accounts/").id(account).view("sshkeys").get(cb);
+ }
+
+ /** Add a new SSH keys */
+ public static void addSshKey(String account, String sshPublicKey,
+ AsyncCallback<SshKeyInfo> cb) {
+ new RestApi("/accounts/").id(account).view("sshkeys")
+ .post(sshPublicKey, cb);
+ }
+
+ /**
+ * Delete SSH keys. For each key to be deleted a separate DELETE request is
+ * fired to the server. The {@code onSuccess} method of the provided callback
+ * is invoked once after all requests succeeded. If any request fails the
+ * callbacks' {@code onFailure} method is invoked. In a failure case it can be
+ * that still some of the keys were successfully deleted.
+ */
+ public static void deleteSshKeys(String account,
+ Set<Integer> sequenceNumbers, AsyncCallback<VoidResult> cb) {
+ CallbackGroup group = new CallbackGroup();
+ for (int seq : sequenceNumbers) {
+ new RestApi("/accounts/").id(account).view("sshkeys").id(seq)
+ .delete(group.add(cb));
+ cb = CallbackGroup.emptyCallback();
+ }
+ group.done();
+ }
+
+ /** Retrieve the HTTP password */
+ public static void getHttpPassword(String account,
+ AsyncCallback<NativeString> cb) {
+ new RestApi("/accounts/").id(account).view("password.http").get(cb);
+ }
+
+ /** Generate a new HTTP password */
+ public static void generateHttpPassword(String account,
+ AsyncCallback<NativeString> cb) {
+ HttpPasswordInput in = HttpPasswordInput.create();
+ in.generate(true);
+ new RestApi("/accounts/").id(account).view("password.http").put(in, cb);
+ }
+
+ /** Clear HTTP password */
+ public static void clearHttpPassword(String account,
+ AsyncCallback<VoidResult> cb) {
+ new RestApi("/accounts/").id(account).view("password.http").delete(cb);
+ }
+
+ private static class HttpPasswordInput extends JavaScriptObject {
+ final native void generate(boolean g) /*-{ if(g)this.generate=g; }-*/;
+
+ static HttpPasswordInput create() {
+ return createObject().cast();
+ }
+
+ protected HttpPasswordInput() {
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index 917c078..28a96149 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -26,6 +26,7 @@
String accountId();
String commentVisibilityLabel();
+ String diffViewLabel();
String maximumPageSizeFieldLabel();
String dateFormatLabel();
String contextWholeFile();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 7175b6a..ab7e5a9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -12,6 +12,7 @@
showUsernameInReviewCategory = Display Person Name In Review Category
maximumPageSizeFieldLabel = Maximum Page Size:
commentVisibilityLabel = Comment Visibility:
+diffViewLabel = Diff View (ChangeScreen2 only):
dateFormatLabel = Date/Time Format:
contextWholeFile = Whole File
buttonSaveChanges = Save Changes
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
index 601d807..eddb56d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
@@ -15,12 +15,36 @@
package com.google.gerrit.client.account;
import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
public class AccountInfo extends JavaScriptObject {
public final native int _account_id() /*-{ return this._account_id || 0; }-*/;
public final native String name() /*-{ return this.name; }-*/;
public final native String email() /*-{ return this.email; }-*/;
+ /**
+ * @return true if the server supplied avatar information about this account.
+ * The information may be an empty list, indicating no avatars are
+ * available, such as when no plugin is installed. This method returns
+ * false if the server did not check on avatars for the account.
+ */
+ public final native boolean has_avatar_info()
+ /*-{ return this.hasOwnProperty('avatars') }-*/;
+
+ public final AvatarInfo avatar(int sz) {
+ JsArray<AvatarInfo> a = avatars();
+ for (int i = 0; a != null && i < a.length(); i++) {
+ AvatarInfo r = a.get(i);
+ if (r.height() == sz) {
+ return r;
+ }
+ }
+ return null;
+ }
+
+ private final native JsArray<AvatarInfo> avatars()
+ /*-{ return this.avatars }-*/;
+
public static native AccountInfo create(int id, String name,
String email) /*-{
return {'_account_id': id, 'name': name, 'email': email};
@@ -28,4 +52,13 @@
protected AccountInfo() {
}
+
+ public static class AvatarInfo extends JavaScriptObject {
+ public final native String url() /*-{ return this.url }-*/;
+ public final native int height() /*-{ return this.height || 0 }-*/;
+ public final native int width() /*-{ return this.width || 0 }-*/;
+
+ protected AvatarInfo() {
+ }
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index f35bd4b..66f676e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -17,14 +17,15 @@
import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.OnEditEnabler;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.client.ContactInformation;
+import com.google.gwt.core.client.JsArray;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
@@ -45,12 +46,6 @@
import com.google.gwtexpui.user.client.AutoCenterDialogBox;
import com.google.gwtjsonrpc.common.AsyncCallback;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
class ContactPanelShort extends Composite {
protected final FlowPanel body;
protected int labelIdx, fieldIdx;
@@ -150,7 +145,6 @@
doSave(null);
}
});
- new OnEditEnabler(save, nameTxt);
emailPick.addChangeHandler(new ChangeHandler() {
@Override
@@ -209,28 +203,19 @@
postLoad();
}
});
- Util.ACCOUNT_SEC
- .myExternalIds(new GerritCallback<List<AccountExternalId>>() {
- public void onSuccess(final List<AccountExternalId> result) {
- if (!isAttached()) {
- return;
- }
- final Set<String> emails = new HashSet<String>();
- for (final AccountExternalId i : result) {
- if (i.getEmailAddress() != null
- && i.getEmailAddress().length() > 0) {
- emails.add(i.getEmailAddress());
- }
- }
- final List<String> addrs = new ArrayList<String>(emails);
- Collections.sort(addrs);
- for (String s : addrs) {
- emailPick.addItem(s);
- }
- haveEmails = true;
- postLoad();
- }
- });
+ AccountApi.getEmails("self", new GerritCallback<JsArray<EmailInfo>>() {
+ @Override
+ public void onSuccess(JsArray<EmailInfo> result) {
+ if (!isAttached()) {
+ return;
+ }
+ for (EmailInfo i : Natives.asList(result)) {
+ emailPick.addItem(i.email());
+ }
+ haveEmails = true;
+ postLoad();
+ }
+ });
}
private void postLoad() {
@@ -255,6 +240,7 @@
currentEmail = userAccount.getPreferredEmail();
nameTxt.setText(userAccount.getFullName());
save.setEnabled(false);
+ new OnEditEnabler(save, nameTxt);
}
private void doRegisterNewEmail() {
@@ -283,13 +269,16 @@
inEmail.setEnabled(false);
register.setEnabled(false);
- Util.ACCOUNT_SEC.registerEmail(addr, new GerritCallback<Account>() {
- public void onSuccess(Account currentUser) {
+ AccountApi.registerEmail("self", addr, new GerritCallback<EmailInfo>() {
+ @Override
+ public void onSuccess(EmailInfo result) {
box.hide();
if (Gerrit.getConfig().getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
currentEmail = addr;
if (emailPick.getItemCount() == 0) {
- onSaveSuccess(currentUser);
+ final Account me = Gerrit.getUserAccount();
+ me.setPreferredEmail(addr);
+ onSaveSuccess(me);
} else {
save.setEnabled(true);
}
@@ -419,6 +408,14 @@
}
}
if (emailPick.getItemCount() > 0) {
+ if (currentEmail == null) {
+ int index = emailListIndexOf("");
+ if (index != -1) {
+ emailPick.removeItem(index);
+ }
+ emailPick.insertItem("", 0);
+ emailPick.setSelectedIndex(0);
+ }
emailPick.setVisible(true);
emailPick.setEnabled(true);
if (canRegisterNewEmail()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EmailInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EmailInfo.java
new file mode 100644
index 0000000..d0bbd8c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/EmailInfo.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.account;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class EmailInfo extends JavaScriptObject {
+ public final native String email() /*-{ return this.email; }-*/;
+ public final native boolean isPreferred() /*-{ return this['preferred'] ? true : false; }-*/;
+ public final native boolean isConfirmationPending() /*-{ return this['pending_confirmation'] ? true : false; }-*/;
+
+ protected EmailInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
index abb1f4d..72ea795 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
@@ -14,12 +14,12 @@
package com.google.gerrit.client.account;
-import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
-
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.i18n.client.LocaleInfo;
@@ -31,13 +31,10 @@
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.clippy.client.CopyableLabel;
-import java.util.List;
-
public class MyPasswordScreen extends SettingsScreen {
private CopyableLabel password;
private Button generatePassword;
private Button clearPassword;
- private AccountExternalId id;
@Override
protected void onInitUI() {
@@ -101,37 +98,48 @@
}
enableUI(false);
- Util.ACCOUNT_SEC
- .myExternalIds(new ScreenLoadCallback<List<AccountExternalId>>(this) {
- public void preDisplay(final List<AccountExternalId> result) {
- AccountExternalId id = null;
- for (AccountExternalId i : result) {
- if (i.isScheme(SCHEME_USERNAME)) {
- id = i;
- break;
- }
- }
- display(id);
- }
- });
+ AccountApi.getUsername("self", new GerritCallback<NativeString>() {
+ @Override
+ public void onSuccess(NativeString user) {
+ Gerrit.getUserAccount().setUserName(user.asString());
+ refreshHttpPassword();
+ }
+
+ @Override
+ public void onFailure(final Throwable caught) {
+ if (RestApi.isNotFound(caught)) {
+ Gerrit.getUserAccount().setUserName(null);
+ display();
+ } else {
+ super.onFailure(caught);
+ }
+ }
+ });
}
- private void display(AccountExternalId id) {
- String user, pass;
- if (id != null) {
- user = id.getSchemeRest();
- pass = id.getPassword();
- } else {
- user = null;
- pass = null;
- }
- this.id = id;
+ private void refreshHttpPassword() {
+ AccountApi.getHttpPassword("self", new ScreenLoadCallback<NativeString>(
+ this) {
+ @Override
+ protected void preDisplay(NativeString httpPassword) {
+ display(httpPassword.asString());
+ }
- Gerrit.getUserAccount().setUserName(user);
+ @Override
+ public void onFailure(final Throwable caught) {
+ if (RestApi.isNotFound(caught)) {
+ display(null);
+ display();
+ } else {
+ super.onFailure(caught);
+ }
+ }
+ });
+ }
+ private void display(String pass) {
password.setText(pass != null ? pass : "");
password.setVisible(pass != null);
-
enableUI(true);
}
@@ -150,12 +158,13 @@
}
private void doGeneratePassword() {
- if (id != null) {
+ if (Gerrit.getUserAccount().getUserName() != null) {
enableUI(false);
- Util.ACCOUNT_SEC.generatePassword(id.getKey(),
- new GerritCallback<AccountExternalId>() {
- public void onSuccess(final AccountExternalId result) {
- display(result);
+ AccountApi.generateHttpPassword("self",
+ new GerritCallback<NativeString>() {
+ @Override
+ public void onSuccess(NativeString newPassword) {
+ display(newPassword.asString());
}
@Override
@@ -167,12 +176,13 @@
}
private void doClearPassword() {
- if (id != null) {
+ if (Gerrit.getUserAccount().getUserName() != null) {
enableUI(false);
- Util.ACCOUNT_SEC.clearPassword(id.getKey(),
- new GerritCallback<AccountExternalId>() {
- public void onSuccess(final AccountExternalId result) {
- display(result);
+ AccountApi.clearHttpPassword("self",
+ new GerritCallback<VoidResult>() {
+ @Override
+ public void onSuccess(VoidResult result) {
+ display(null);
}
@Override
@@ -184,9 +194,9 @@
}
private void enableUI(boolean on) {
- on &= id != null;
+ on &= Gerrit.getUserAccount().getUserName() != null;
generatePassword.setEnabled(on);
- clearPassword.setVisible(on && id.getPassword() != null);
+ clearPassword.setVisible(on && !"".equals(password.getText()));
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index 639a1cf..dbc8480 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -48,6 +48,7 @@
private ListBox dateFormat;
private ListBox timeFormat;
private ListBox commentVisibilityStrategy;
+ private ListBox diffView;
private Button save;
@Override
@@ -82,6 +83,16 @@
AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_ALL.name()
);
+ diffView = new ListBox();
+ diffView.addItem(
+ com.google.gerrit.client.changes.Util.C.sideBySide(),
+ AccountGeneralPreferences.DiffView.SIDE_BY_SIDE.name()
+ );
+ diffView.addItem(
+ com.google.gerrit.client.changes.Util.C.unifiedDiff(),
+ AccountGeneralPreferences.DiffView.UNIFIED_DIFF.name()
+ );
+
Date now = new Date();
dateFormat = new ListBox();
for (AccountGeneralPreferences.DateFormat fmt : AccountGeneralPreferences.DateFormat
@@ -118,7 +129,7 @@
relativeDateInChangeTable = new CheckBox(Util.C.showRelativeDateInChangeTable());
- final Grid formGrid = new Grid(9, 2);
+ final Grid formGrid = new Grid(10, 2);
int row = 0;
formGrid.setText(row, labelIdx, "");
@@ -157,6 +168,10 @@
formGrid.setWidget(row, fieldIdx, commentVisibilityStrategy);
row++;
+ formGrid.setText(row, labelIdx, Util.C.diffViewLabel());
+ formGrid.setWidget(row, fieldIdx, diffView);
+ row++;
+
add(formGrid);
save = new Button(Util.C.buttonSaveChanges());
@@ -180,6 +195,7 @@
e.listenTo(timeFormat);
e.listenTo(relativeDateInChangeTable);
e.listenTo(commentVisibilityStrategy);
+ e.listenTo(diffView);
}
@Override
@@ -203,6 +219,7 @@
timeFormat.setEnabled(on);
relativeDateInChangeTable.setEnabled(on);
commentVisibilityStrategy.setEnabled(on);
+ diffView.setEnabled(on);
}
private void display(final AccountGeneralPreferences p) {
@@ -218,8 +235,11 @@
p.getTimeFormat());
relativeDateInChangeTable.setValue(p.isRelativeDateInChangeTable());
setListBox(commentVisibilityStrategy,
- AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_MOST_RECENT,
+ AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_RECENT,
p.getCommentVisibilityStrategy());
+ setListBox(diffView,
+ AccountGeneralPreferences.DiffView.SIDE_BY_SIDE,
+ p.getDiffView());
}
private void setListBox(final ListBox f, final short defaultValue,
@@ -285,8 +305,11 @@
AccountGeneralPreferences.TimeFormat.values()));
p.setRelativeDateInChangeTable(relativeDateInChangeTable.getValue());
p.setCommentVisibilityStrategy(getListBox(commentVisibilityStrategy,
- CommentVisibilityStrategy.EXPAND_MOST_RECENT,
+ CommentVisibilityStrategy.EXPAND_RECENT,
CommentVisibilityStrategy.values()));
+ p.setDiffView(getListBox(diffView,
+ AccountGeneralPreferences.DiffView.SIDE_BY_SIDE,
+ AccountGeneralPreferences.DiffView.values()));
enable(false);
save.setEnabled(false);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
index 97b2efb..2c29ab2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
@@ -29,7 +29,9 @@
if (Gerrit.getConfig().getSshdAddress() != null) {
link(Util.C.tabSshKeys(), PageLinks.SETTINGS_SSHKEYS);
}
- link(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
+ if (!Gerrit.getConfig().isGitBasicAuth()) {
+ link(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
+ }
link(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT);
link(Util.C.tabMyGroups(), PageLinks.SETTINGS_MYGROUPS);
if (Gerrit.getConfig().isUseContributorAgreements()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshKeyInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshKeyInfo.java
new file mode 100644
index 0000000..a4fed8b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshKeyInfo.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.account;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class SshKeyInfo extends JavaScriptObject {
+ public final native int seq() /*-{ return this.seq || 0; }-*/;
+ public final native String sshPublicKey() /*-{ return this.ssh_public_key; }-*/;
+ public final native String encodedKey() /*-{ return this.encoded_key; }-*/;
+ public final native String algorithm() /*-{ return this.algorithm; }-*/;
+ public final native String comment() /*-{ return this.comment; }-*/;
+ public final native boolean isValid() /*-{ return this['valid'] ? true : false; }-*/;
+
+ protected SshKeyInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
index aaadc5e6..0dfb520 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
@@ -16,13 +16,15 @@
import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.ComplexDisclosurePanel;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gerrit.client.ui.SmallHeading;
import com.google.gerrit.common.data.SshHostKey;
import com.google.gerrit.common.errors.InvalidSshKeyException;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gwt.core.client.JsArray;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
@@ -40,7 +42,6 @@
import com.google.gwtexpui.clippy.client.CopyableLabel;
import com.google.gwtexpui.globalkey.client.NpTextArea;
import com.google.gwtjsonrpc.client.RemoteJsonException;
-import com.google.gwtjsonrpc.common.VoidResult;
import java.util.Collections;
import java.util.HashSet;
@@ -157,11 +158,11 @@
final String txt = addTxt.getText();
if (txt != null && txt.length() > 0) {
addNew.setEnabled(false);
- Util.ACCOUNT_SEC.addSshKey(txt, new GerritCallback<AccountSshKey>() {
- public void onSuccess(final AccountSshKey result) {
+ AccountApi.addSshKey("self", txt, new GerritCallback<SshKeyInfo>() {
+ public void onSuccess(final SshKeyInfo k) {
addNew.setEnabled(true);
addTxt.setText("");
- keys.addOneKey(result);
+ keys.addOneKey(k);
if (!keys.isVisible()) {
showAddKeyBlock(false);
setKeyTableVisible(true);
@@ -195,24 +196,27 @@
@Override
protected void onLoad() {
super.onLoad();
-
- Util.ACCOUNT_SEC.mySshKeys(new GerritCallback<List<AccountSshKey>>() {
- public void onSuccess(final List<AccountSshKey> result) {
- keys.display(result);
- if (result.isEmpty() && keys.isVisible()) {
- showAddKeyBlock(true);
+ refreshSshKeys();
+ Gerrit.SYSTEM_SVC.daemonHostKeys(new GerritCallback<List<SshHostKey>>() {
+ public void onSuccess(final List<SshHostKey> result) {
+ serverKeys.clear();
+ for (final SshHostKey keyInfo : result) {
+ serverKeys.add(new SshHostKeyPanel(keyInfo));
}
if (++loadCount == 2) {
display();
}
}
});
+ }
- Gerrit.SYSTEM_SVC.daemonHostKeys(new GerritCallback<List<SshHostKey>>() {
- public void onSuccess(final List<SshHostKey> result) {
- serverKeys.clear();
- for (final SshHostKey keyInfo : result) {
- serverKeys.add(new SshHostKeyPanel(keyInfo));
+ private void refreshSshKeys() {
+ AccountApi.getSshKeys("self", new GerritCallback<JsArray<SshKeyInfo>>() {
+ @Override
+ public void onSuccess(JsArray<SshKeyInfo> result) {
+ keys.display(Natives.asList(result));
+ if (result.length() == 0 && keys.isVisible()) {
+ showAddKeyBlock(true);
}
if (++loadCount == 2) {
display();
@@ -229,7 +233,7 @@
addKeyBlock.setVisible(show);
}
- private class SshKeyTable extends FancyFlexTable<AccountSshKey> {
+ private class SshKeyTable extends FancyFlexTable<SshKeyInfo> {
private ValueChangeHandler<Boolean> updateDeleteHandler;
SshKeyTable() {
@@ -255,44 +259,53 @@
}
void deleteChecked() {
- final HashSet<AccountSshKey.Id> ids = new HashSet<AccountSshKey.Id>();
+ final HashSet<Integer> sequenceNumbers = new HashSet<Integer>();
for (int row = 1; row < table.getRowCount(); row++) {
- final AccountSshKey k = getRowItem(row);
+ final SshKeyInfo k = getRowItem(row);
if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
- ids.add(k.getKey());
+ sequenceNumbers.add(k.seq());
}
}
- if (ids.isEmpty()) {
+ if (sequenceNumbers.isEmpty()) {
updateDeleteButton();
} else {
- Util.ACCOUNT_SEC.deleteSshKeys(ids, new GerritCallback<VoidResult>() {
- public void onSuccess(final VoidResult result) {
- for (int row = 1; row < table.getRowCount();) {
- final AccountSshKey k = getRowItem(row);
- if (k != null && ids.contains(k.getKey())) {
- table.removeRow(row);
- } else {
- row++;
+ deleteKey.setEnabled(false);
+ AccountApi.deleteSshKeys("self", sequenceNumbers,
+ new GerritCallback<VoidResult>() {
+ public void onSuccess(VoidResult result) {
+ for (int row = 1; row < table.getRowCount();) {
+ final SshKeyInfo k = getRowItem(row);
+ if (k != null && sequenceNumbers.contains(k.seq())) {
+ table.removeRow(row);
+ } else {
+ row++;
+ }
+ }
+ if (table.getRowCount() == 1) {
+ display(Collections.<SshKeyInfo> emptyList());
+ } else {
+ updateDeleteButton();
+ }
}
- }
- if (table.getRowCount() == 1) {
- display(Collections.<AccountSshKey> emptyList());
- } else {
- updateDeleteButton();
- }
- }
- });
+
+ @Override
+ public void onFailure(Throwable caught) {
+ refreshSshKeys();
+ updateDeleteButton();
+ super.onFailure(caught);
+ }
+ });
}
}
- void display(final List<AccountSshKey> result) {
+ void display(final List<SshKeyInfo> result) {
if (result.isEmpty()) {
setKeyTableVisible(false);
showAddKeyBlock(true);
} else {
while (1 < table.getRowCount())
table.removeRow(table.getRowCount() - 1);
- for (final AccountSshKey k : result) {
+ for (final SshKeyInfo k : result) {
addOneKey(k);
}
setKeyTableVisible(true);
@@ -300,7 +313,7 @@
}
}
- void addOneKey(final AccountSshKey k) {
+ void addOneKey(final SshKeyInfo k) {
final FlexCellFormatter fmt = table.getFlexCellFormatter();
final int row = table.getRowCount();
table.insertRow(row);
@@ -318,13 +331,13 @@
table.setText(row, 2, Util.C.sshKeyInvalid());
fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().sshKeyPanelInvalid());
}
- table.setText(row, 3, k.getAlgorithm());
+ table.setText(row, 3, k.algorithm());
- CopyableLabel keyLabel = new CopyableLabel(k.getSshPublicKey());
- keyLabel.setPreviewText(elide(k.getEncodedKey(), 40));
+ CopyableLabel keyLabel = new CopyableLabel(k.sshPublicKey());
+ keyLabel.setPreviewText(elide(k.encodedKey(), 40));
table.setWidget(row, 4, keyLabel);
- table.setText(row, 5, k.getComment());
+ table.setText(row, 5, k.comment());
fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().sshKeyPanelEncodedKey());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
new file mode 100644
index 0000000..b167b5f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionButton.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.actions;
+
+import com.google.gerrit.client.api.ActionContext;
+import com.google.gerrit.client.api.ChangeGlue;
+import com.google.gerrit.client.api.RevisionGlue;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+public class ActionButton extends Button implements ClickHandler {
+ private final ChangeInfo change;
+ private final RevisionInfo revision;
+ private final ActionInfo action;
+ private ActionContext ctx;
+
+ public ActionButton(ChangeInfo change, ActionInfo action) {
+ this(change, null, action);
+ }
+
+ public ActionButton(ChangeInfo change, RevisionInfo revision, ActionInfo action) {
+ super(new SafeHtmlBuilder()
+ .openDiv()
+ .append(action.label())
+ .closeDiv());
+ setStyleName("");
+ setTitle(action.title());
+ setEnabled(action.enabled());
+ addClickHandler(this);
+
+ this.change = change;
+ this.revision = revision;
+ this.action = action;
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ if (ctx != null && ctx.has_popup()) {
+ ctx.hide();
+ ctx = null;
+ return;
+ }
+
+ if (revision != null) {
+ RevisionGlue.onAction(change, revision, action, this);
+ } else {
+ ChangeGlue.onAction(change, action, this);
+ }
+ }
+
+ @Override
+ public void onUnload() {
+ if (ctx != null) {
+ if (ctx.has_popup()) {
+ ctx.hide();
+ }
+ ctx = null;
+ }
+ super.onUnload();
+ }
+
+ public void link(ActionContext ctx) {
+ this.ctx = ctx;
+ }
+
+ public void unlink() {
+ ctx = null;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionInfo.java
new file mode 100644
index 0000000..ef0c4b5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/actions/ActionInfo.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.actions;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ActionInfo extends JavaScriptObject {
+
+ public final native String id() /*-{ return this.id; }-*/;
+ public final native String method() /*-{ return this.method; }-*/;
+ public final native String label() /*-{ return this.label; }-*/;
+ public final native String title() /*-{ return this.title; }-*/;
+ public final native boolean enabled() /*-{ return this.enabled || false; }-*/;
+
+ protected ActionInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
index 344104d..b734b41 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -93,9 +93,8 @@
public AccessSectionEditor(ProjectAccess access) {
projectAccess = access;
-
- permissionSelector =
- new ValueListBox<String>(PermissionNameRenderer.INSTANCE);
+ permissionSelector = new ValueListBox<String>(
+ new PermissionNameRenderer(access.getCapabilities()));
permissionSelector.addValueChangeHandler(new ValueChangeHandler<String>() {
@Override
public void onValueChange(ValueChangeEvent<String> event) {
@@ -222,12 +221,15 @@
List<String> perms = new ArrayList<String>();
if (AccessSection.GLOBAL_CAPABILITIES.equals(value.getName())) {
- for (String varName : Util.C.capabilityNames().keySet()) {
+ for (String varName : projectAccess.getCapabilities().keySet()) {
addPermission(varName, perms);
}
} else if (RefConfigSection.isValid(value.getName())) {
for (LabelType t : projectAccess.getLabelTypes().getLabelTypes()) {
- addPermission(Permission.LABEL + t.getName(), perms);
+ addPermission(Permission.forLabel(t.getName()), perms);
+ }
+ for (LabelType t : projectAccess.getLabelTypes().getLabelTypes()) {
+ addPermission(Permission.forLabelAs(t.getName()), perms);
}
for (String varName : Util.C.permissionNames().keySet()) {
addPermission(varName, perms);
@@ -282,7 +284,7 @@
@Override
public PermissionEditor create(int index) {
PermissionEditor subEditor =
- new PermissionEditor(projectAccess.getProjectName(), readOnly, value,
+ new PermissionEditor(projectAccess, readOnly, value,
projectAccess.getLabelTypes());
permissionContainer.insert(subEditor, index);
return subEditor;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
index ea836c6..8e0d22b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
@@ -109,8 +109,6 @@
});
groupNamePanel.add(saveName);
add(groupNamePanel);
-
- new OnEditEnabler(saveName, groupNameTxt);
}
private void initOwner() {
@@ -120,8 +118,9 @@
ownerTxtBox = new NpTextBox();
ownerTxtBox.setVisibleLength(60);
+ final AccountGroupSuggestOracle accountGroupOracle = new AccountGroupSuggestOracle();
ownerTxt = new SuggestBox(new RPCSuggestOracle(
- new AccountGroupSuggestOracle()), ownerTxtBox);
+ accountGroupOracle), ownerTxtBox);
ownerTxt.setStyleName(Gerrit.RESOURCES.css().groupOwnerTextBox());
ownerPanel.add(ownerTxt);
@@ -132,7 +131,9 @@
public void onClick(final ClickEvent event) {
final String newOwner = ownerTxt.getText().trim();
if (newOwner.length() > 0) {
- GroupApi.setGroupOwner(getGroupUUID(), newOwner,
+ AccountGroup.UUID ownerUuid = accountGroupOracle.getUUID(newOwner);
+ String ownerId = ownerUuid != null ? ownerUuid.get() : newOwner;
+ GroupApi.setGroupOwner(getGroupUUID(), ownerId,
new GerritCallback<GroupInfo>() {
public void onSuccess(final GroupInfo result) {
updateOwnerGroup(result);
@@ -144,8 +145,6 @@
});
ownerPanel.add(saveOwner);
add(ownerPanel);
-
- new OnEditEnabler(saveOwner, ownerTxtBox);
}
private void initDescription() {
@@ -174,8 +173,6 @@
});
vp.add(saveDesc);
add(vp);
-
- new OnEditEnabler(saveDesc, descTxt);
}
private void initGroupOptions() {
@@ -225,5 +222,8 @@
saveOwner.setVisible(canModify);
saveDesc.setVisible(canModify);
saveGroupOptions.setVisible(canModify);
+ new OnEditEnabler(saveDesc, descTxt);
+ new OnEditEnabler(saveName, groupNameTxt);
+ new OnEditEnabler(saveOwner, ownerTxtBox);
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index 51ff978..7aaf04d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -58,6 +58,7 @@
private Button delInclude;
private FlowPanel noMembersInfo;
+ private AccountGroupSuggestOracle accountGroupSuggestOracle;
public AccountGroupMembersScreen(final GroupInfo toShow, final String token) {
super(toShow, token);
@@ -109,9 +110,10 @@
}
private void initIncludeList() {
+ accountGroupSuggestOracle = new AccountGroupSuggestOracle();
addIncludeBox =
new AddMemberBox(Util.C.buttonAddIncludedGroup(),
- Util.C.defaultAccountGroupName(), new AccountGroupSuggestOracle());
+ Util.C.defaultAccountGroupName(), accountGroupSuggestOracle);
addIncludeBox.addClickHandler(new ClickHandler() {
@Override
@@ -187,13 +189,18 @@
}
void doAddNewInclude() {
- final String groupName = addIncludeBox.getText();
+ String groupName = addIncludeBox.getText();
if (groupName.length() == 0) {
return;
}
+ AccountGroup.UUID uuid = accountGroupSuggestOracle.getUUID(groupName);
+ if (uuid == null) {
+ return;
+ }
+
addIncludeBox.setEnabled(false);
- GroupApi.addIncludedGroup(getGroupUUID(), groupName,
+ GroupApi.addIncludedGroup(getGroupUUID(), uuid.get(),
new GerritCallback<GroupInfo>() {
public void onSuccess(final GroupInfo result) {
addIncludeBox.setEnabled(true);
@@ -290,28 +297,12 @@
return str == null ? "" : str;
}
};
- int insertPosition = table.getRowCount();
- int left = 1;
- int right = table.getRowCount() - 1;
- while (left <= right) {
- int middle = (left + right) >>> 1; // (left+right)/2
- AccountInfo i = getRowItem(middle);
- int cmp = c.compare(i, info);
-
- if (cmp < 0) {
- left = middle + 1;
- } else if (cmp > 0) {
- right = middle - 1;
- } else {
- // group is already contained in the table
- return;
- }
+ int insertPos = getInsertRow(c, info);
+ if (insertPos >= 0) {
+ table.insertRow(insertPos);
+ applyDataRowStyle(insertPos);
+ populate(insertPos, info);
}
- insertPosition = left;
-
- table.insertRow(insertPosition);
- applyDataRowStyle(insertPosition);
- populate(insertPosition, info);
}
void populate(final int row, final AccountInfo i) {
@@ -405,29 +396,12 @@
return (str == null) ? "" : str;
}
};
-
- int insertPosition = table.getRowCount();
- int left = 1;
- int right = table.getRowCount() - 1;
- while (left <= right) {
- int middle = (left + right) >>> 1; // (left+right)/2
- GroupInfo i = getRowItem(middle);
- int cmp = c.compare(i, info);
-
- if (cmp < 0) {
- left = middle + 1;
- } else if (cmp > 0) {
- right = middle - 1;
- } else {
- // group is already contained in the table
- return;
- }
+ int insertPos = getInsertRow(c, info);
+ if (insertPos >= 0) {
+ table.insertRow(insertPos);
+ applyDataRowStyle(insertPos);
+ populate(insertPos, info);
}
- insertPosition = left;
-
- table.insertRow(insertPosition);
- applyDataRowStyle(insertPosition);
- populate(insertPosition, info);
}
void populate(final int row, final GroupInfo i) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index f4c0b55..7b0cd5f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -42,6 +42,7 @@
String useContributorAgreements();
String useSignedOffBy();
String requireChangeID();
+ String headingMaxObjectSizeLimit();
String headingGroupOptions();
String isVisibleToAll();
String buttonSaveGroupOptions();
@@ -89,7 +90,6 @@
String initialRevision();
String buttonAddBranch();
String buttonDeleteBranch();
- String branchDeletionOpenChanges();
String groupItemHelp();
@@ -125,8 +125,6 @@
String refErrorPrintable();
String errorsMustBeFixed();
- Map<String, String> capabilityNames();
-
String sectionTypeReference();
String sectionTypeSection();
Map<String, String> sectionNames();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 178db6b..4459bac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -24,6 +24,7 @@
useContributorAgreements = Require a valid contributor agreement to upload
useSignedOffBy = Require <code>Signed-off-by</code> in commit message
requireChangeID = Require <code>Change-Id</code> in commit message
+headingMaxObjectSizeLimit = Maximum Git object size limit
headingGroupOptions = Group Options
isVisibleToAll = Make group visible to all registered users.
buttonSaveGroupOptions = Save Group Options
@@ -68,8 +69,6 @@
initialRevision = Initial Revision
buttonAddBranch = Create Branch
buttonDeleteBranch = Delete
-branchDeletionOpenChanges = The following branches were not deleted \
-because they have open changes:
groupItemHelp = group
@@ -144,41 +143,6 @@
refErrorPrintable = References may contain only printable characters
errorsMustBeFixed = Errors must be fixed before committing changes.
-# Capability Names
-capabilityNames = \
- accessDatabase, \
- administrateServer, \
- createAccount, \
- createGroup, \
- createProject, \
- emailReviewers, \
- flushCaches, \
- killTask, \
- priority, \
- queryLimit, \
- runGC, \
- startReplication, \
- streamEvents, \
- viewCaches, \
- viewConnections, \
- viewQueue
-accessDatabase = Access Database
-administrateServer = Administrate Server
-createAccount = Create Account
-createGroup = Create Group
-createProject = Create Project
-emailReviewers = Email Reviewers
-flushCaches = Flush Caches
-killTask = Kill Task
-priority = Priority
-queryLimit = Query Limit
-runGC = Run Garbage Collection
-startReplication = Start Replication
-streamEvents = Stream Events
-viewCaches = View Caches
-viewConnections = View Connections
-viewQueue = View Queue
-
# Section Names
sectionTypeReference = Reference:
sectionTypeSection = Section:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
index 3b4d7d4..6b1269d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
@@ -19,9 +19,13 @@
public interface AdminMessages extends Messages {
String group(String name);
String label(String name);
+ String labelAs(String name);
String project(String name);
String deletedGroup(int id);
String deletedReference(String name);
String deletedSection(String name);
+
+ String effectiveMaxObjectSizeLimit(String effectiveMaxObjectSizeLimit);
+ String globalMaxObjectSizeLimit(String globalMaxObjectSizeLimit);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
index 7f8cd56..cb3784f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
@@ -1,6 +1,9 @@
group = Group {0}
label = Label {0}
+labelAs = Label {0} (On Behalf Of)
project = Project {0}
deletedGroup = Deleted Group {0}
deletedReference = Reference {0} was deleted
deletedSection = Section {0} was deleted
+effectiveMaxObjectSizeLimit = effective: {0}
+globalMaxObjectSizeLimit = The global max object size limit is set to {0}. The limit cannot be increased on project level.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index dac0b6a..bed6b4a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -23,6 +23,7 @@
import com.google.gerrit.client.ui.FilteredUserInterface;
import com.google.gerrit.client.ui.IgnoreOutdatedFilterResultsCallbackWrapper;
import com.google.gerrit.common.PageLinks;
+import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.http.client.URL;
@@ -55,10 +56,10 @@
protected void onLoad() {
super.onLoad();
display();
- refresh();
+ refresh(false);
}
- private void refresh() {
+ private void refresh(final boolean open) {
setToken(subname == null || "".equals(subname) ? ADMIN_GROUPS
: ADMIN_GROUPS + "?filter=" + URL.encodeQueryString(subname));
GroupMap.match(subname,
@@ -66,8 +67,13 @@
new GerritCallback<GroupMap>() {
@Override
public void onSuccess(GroupMap result) {
- groups.display(result, subname);
- groups.finishDisplay();
+ if (open && result.values().length() > 0) {
+ Gerrit.display(PageLinks.toGroup(
+ result.values().get(0).getGroupUUID()));
+ } else {
+ groups.display(result, subname);
+ groups.finishDisplay();
+ }
}
}));
}
@@ -99,7 +105,7 @@
@Override
public void onKeyUp(KeyUpEvent event) {
subname = filterTxt.getValue();
- refresh();
+ refresh(event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER);
}
});
hp.add(filterTxt);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
index 9848c18..5222751 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -24,6 +24,7 @@
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.ProjectAccess;
import com.google.gerrit.common.data.RefConfigSection;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.GWT;
@@ -107,17 +108,19 @@
private PermissionRange.WithDefaults validRange;
private boolean isDeleted;
- public PermissionEditor(Project.NameKey projectName,
+ public PermissionEditor(ProjectAccess projectAccess,
boolean readOnly,
AccessSection section,
LabelTypes labelTypes) {
this.readOnly = readOnly;
this.section = section;
- this.projectName = projectName;
+ this.projectName = projectAccess.getProjectName();
this.labelTypes = labelTypes;
- normalName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
- deletedName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
+ PermissionNameRenderer nameRenderer =
+ new PermissionNameRenderer(projectAccess.getCapabilities());
+ normalName = new ValueLabel<String>(nameRenderer);
+ deletedName = new ValueLabel<String>(nameRenderer);
initWidget(uiBinder.createAndBindUi(this));
groupToAdd.setProject(projectName);
@@ -262,7 +265,7 @@
public void setValue(Permission value) {
this.value = value;
- if (value.isLabel()) {
+ if (Permission.hasRange(value.getName())) {
LabelType lt = labelTypes.byLabel(value.getLabel());
if (lt != null) {
validRange = new PermissionRange.WithDefaults(
@@ -277,7 +280,7 @@
validRange = null;
}
- if (value != null && Permission.OWNER.equals(value.getName())) {
+ if (Permission.OWNER.equals(value.getName())) {
exclusiveGroup.setEnabled(false);
} else {
exclusiveGroup.setEnabled(!readOnly);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java
index ad3473c..d8ee195 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java
@@ -22,37 +22,54 @@
import java.util.Map;
class PermissionNameRenderer implements Renderer<String> {
- static final PermissionNameRenderer INSTANCE = new PermissionNameRenderer();
-
- private static final Map<String, String> all;
+ private static final Map<String, String> permissions;
static {
- all = new HashMap<String, String>();
- for (Map.Entry<String, String> e : Util.C.capabilityNames().entrySet()) {
- all.put(e.getKey(), e.getValue());
- all.put(e.getKey().toLowerCase(), e.getValue());
- }
+ permissions = new HashMap<String, String>();
for (Map.Entry<String, String> e : Util.C.permissionNames().entrySet()) {
- all.put(e.getKey(), e.getValue());
- all.put(e.getKey().toLowerCase(), e.getValue());
+ permissions.put(e.getKey(), e.getValue());
+ permissions.put(e.getKey().toLowerCase(), e.getValue());
}
}
+ private final Map<String, String> fromServer;
+
+ PermissionNameRenderer(Map<String, String> allFromOutside) {
+ fromServer = allFromOutside;
+ }
+
@Override
public String render(String varName) {
- if (Permission.isLabel(varName)) {
- return Util.M.label(new Permission(varName).getLabel());
+ if (Permission.isLabelAs(varName)) {
+ return Util.M.labelAs(Permission.extractLabel(varName));
+ } else if (Permission.isLabel(varName)) {
+ return Util.M.label(Permission.extractLabel(varName));
}
- String desc = all.get(varName);
- if (desc == null) {
- desc = all.get(varName.toLowerCase());
+ String desc = permissions.get(varName);
+ if (desc != null) {
+ return desc;
}
- return desc != null ? desc : varName;
+
+ desc = fromServer.get(varName);
+ if (desc != null) {
+ return desc;
+ }
+
+ desc = permissions.get(varName.toLowerCase());
+ if (desc != null) {
+ return desc;
+ }
+
+ desc = fromServer.get(varName.toLowerCase());
+ if (desc != null) {
+ return desc;
+ }
+ return varName;
}
@Override
public void render(String object, Appendable appendable) throws IOException {
appendable.append(render(object));
}
-}
\ No newline at end of file
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
index 32bc469..49a9aa4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
@@ -120,7 +120,7 @@
history.getStyle().setDisplay(Display.NONE);
}
- addSection.setVisible(value != null && editing && (!value.getOwnerOf().isEmpty() || value.canUpload()));
+ addSection.setVisible(editing && (!value.getOwnerOf().isEmpty() || value.canUpload()));
}
@Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
index 96824f3..ef7f560 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -18,7 +18,12 @@
import static com.google.gerrit.common.ProjectAccessUtil.removeEmptyPermissionsAndSections;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.config.CapabilityInfo;
+import com.google.gerrit.client.config.ConfigServerApi;
+import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.AccessSection;
@@ -33,6 +38,7 @@
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Label;
@@ -41,8 +47,10 @@
import com.google.gwtexpui.globalkey.client.NpTextArea;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
public class ProjectAccessScreen extends ProjectScreen {
@@ -90,6 +98,8 @@
private ProjectAccess access;
+ private NativeMap<CapabilityInfo> capabilityMap;
+
public ProjectAccessScreen(final Project.NameKey toShow) {
super(toShow);
}
@@ -107,18 +117,36 @@
@Override
protected void onLoad() {
super.onLoad();
+ CallbackGroup cbs = new CallbackGroup();
+ ConfigServerApi.capabilities(
+ cbs.add(new AsyncCallback<NativeMap<CapabilityInfo>>() {
+ @Override
+ public void onSuccess(NativeMap<CapabilityInfo> result) {
+ capabilityMap = result;
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ // Handled by ScreenLoadCallback.onFailure().
+ }
+ }));
Util.PROJECT_SVC.projectAccess(getProjectKey(),
- new ScreenLoadCallback<ProjectAccess>(this) {
+ cbs.addFinal(new ScreenLoadCallback<ProjectAccess>(this) {
@Override
public void preDisplay(ProjectAccess access) {
displayReadOnly(access);
}
- });
+ }));
savedPanel = ACCESS;
}
private void displayReadOnly(ProjectAccess access) {
this.access = access;
+ Map<String, String> allCapabilities = new HashMap<String, String>();
+ for (CapabilityInfo c : Natives.asList(capabilityMap.values())) {
+ allCapabilities.put(c.id(), c.name());
+ }
+ this.access.setCapabilities(allCapabilities);
accessEditor.setEditing(false);
UIObject.setVisible(editTools, !access.getOwnerOf().isEmpty() || access.canUpload());
edit.setEnabled(!access.getOwnerOf().isEmpty() || access.canUpload());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index a7c6a23..04d407e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -19,16 +19,19 @@
import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.access.AccessMap;
+import com.google.gerrit.client.access.ProjectAccessInfo;
+import com.google.gerrit.client.projects.BranchInfo;
+import com.google.gerrit.client.projects.ProjectApi;
import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.BranchLink;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gerrit.client.ui.HintTextBox;
-import com.google.gerrit.common.data.AddBranchResult;
-import com.google.gerrit.common.data.ListBranchesResult;
import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.dom.client.ClickEvent;
@@ -36,23 +39,25 @@
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.TextBox;
-import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ProjectBranchesScreen extends ProjectScreen {
- private BranchesTable branches;
+ private BranchesTable branchTable;
private Button delBranch;
private Button addBranch;
private HintTextBox nameTxtBox;
@@ -66,39 +71,41 @@
@Override
protected void onLoad() {
super.onLoad();
- Util.PROJECT_SVC.listBranches(getProjectKey(),
- new ScreenLoadCallback<ListBranchesResult>(this) {
+ addPanel.setVisible(false);
+ AccessMap.get(getProjectKey(),
+ new GerritCallback<ProjectAccessInfo>() {
@Override
- public void preDisplay(final ListBranchesResult result) {
- if (result.getNoRepository()) {
- branches.setVisible(false);
- addPanel.setVisible(false);
- delBranch.setVisible(false);
-
- Label no = new Label(Util.C.errorNoGitRepository());
- no.setStyleName(Gerrit.RESOURCES.css().smallHeading());
- add(no);
-
- } else {
- enableForm(true);
- display(result.getBranches());
- addPanel.setVisible(result.getCanAdd());
- }
+ public void onSuccess(ProjectAccessInfo result) {
+ addPanel.setVisible(result.canAddRefs());
}
});
+ refreshBranches();
savedPanel = BRANCH;
}
- private void display(final List<Branch> listBranches) {
- branches.display(listBranches);
- delBranch.setVisible(branches.hasBranchCanDelete());
+ private void refreshBranches() {
+ ProjectApi.getBranches(getProjectKey(),
+ new ScreenLoadCallback<JsArray<BranchInfo>>(this) {
+ @Override
+ public void preDisplay(final JsArray<BranchInfo> result) {
+ Set<String> checkedRefs = branchTable.getCheckedRefs();
+ display(Natives.asList(result));
+ branchTable.setChecked(checkedRefs);
+ updateForm();
+ }
+ });
}
- private void enableForm(final boolean on) {
- delBranch.setEnabled(on);
- addBranch.setEnabled(on);
- nameTxtBox.setEnabled(on);
- irevTxtBox.setEnabled(on);
+ private void display(final List<BranchInfo> branches) {
+ branchTable.display(branches);
+ delBranch.setVisible(branchTable.hasBranchCanDelete());
+ }
+
+ private void updateForm() {
+ branchTable.updateDeleteButton();
+ addBranch.setEnabled(true);
+ nameTxtBox.setEnabled(true);
+ irevTxtBox.setEnabled(true);
}
@Override
@@ -149,29 +156,29 @@
addPanel.add(addGrid);
addPanel.add(addBranch);
- branches = new BranchesTable();
+ branchTable = new BranchesTable();
delBranch = new Button(Util.C.buttonDeleteBranch());
delBranch.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
- branches.deleteChecked();
+ branchTable.deleteChecked();
}
});
- add(branches);
+ add(branchTable);
add(delBranch);
add(addPanel);
}
private void doAddNewBranch() {
- final String branchName = nameTxtBox.getText();
+ final String branchName = nameTxtBox.getText().trim();
if ("".equals(branchName)) {
nameTxtBox.setFocus(true);
return;
}
- final String rev = irevTxtBox.getText();
+ final String rev = irevTxtBox.getText().trim();
if ("".equals(rev)) {
irevTxtBox.setText("HEAD");
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@@ -185,62 +192,23 @@
}
addBranch.setEnabled(false);
- Util.PROJECT_SVC.addBranch(getProjectKey(), branchName, rev,
- new GerritCallback<AddBranchResult>() {
- public void onSuccess(final AddBranchResult result) {
- addBranch.setEnabled(true);
- if (!result.hasError()) {
- nameTxtBox.setText("");
- irevTxtBox.setText("");
- display(result.getListBranchesResult().getBranches());
- } else {
- final AddBranchResult.Error error = result.getError();
- final String msg;
- switch (error.getType()) {
- case INVALID_NAME:
- selectAllAndFocus(nameTxtBox);
- msg = Gerrit.M.invalidBranchName(branchName);
- break;
-
- case INVALID_REVISION:
- selectAllAndFocus(irevTxtBox);
- msg = Gerrit.M.invalidRevision(rev);
- break;
-
- case BRANCH_CREATION_NOT_ALLOWED_UNDER_REFNAME_PREFIX:
- selectAllAndFocus(nameTxtBox);
- msg =
- Gerrit.M.branchCreationNotAllowedUnderRefnamePrefix(error
- .getRefname());
- break;
-
- case BRANCH_ALREADY_EXISTS:
- selectAllAndFocus(nameTxtBox);
- msg = Gerrit.M.branchAlreadyExists(error.getRefname());
- break;
-
- case BRANCH_CREATION_CONFLICT:
- selectAllAndFocus(nameTxtBox);
- msg =
- Gerrit.M.branchCreationConflict(branchName,
- error.getRefname());
- break;
-
- default:
- msg =
- Gerrit.M.branchCreationFailed(branchName,
- error.toString());
- }
- new ErrorDialog(msg).center();
- }
- }
-
+ ProjectApi.createBranch(getProjectKey(), branchName, rev,
+ new GerritCallback<BranchInfo>() {
@Override
- public void onFailure(final Throwable caught) {
+ public void onSuccess(BranchInfo branch) {
addBranch.setEnabled(true);
- super.onFailure(caught);
+ nameTxtBox.setText("");
+ irevTxtBox.setText("");
+ branchTable.insert(branch);
}
- });
+
+ @Override
+ public void onFailure(Throwable caught) {
+ addBranch.setEnabled(true);
+ selectAllAndFocus(nameTxtBox);
+ new ErrorDialog(caught.getMessage()).center();
+ }
+ });
}
private static void selectAllAndFocus(final TextBox textBox) {
@@ -248,7 +216,8 @@
textBox.setFocus(true);
}
- private class BranchesTable extends FancyFlexTable<Branch> {
+ private class BranchesTable extends FancyFlexTable<BranchInfo> {
+ private ValueChangeHandler<Boolean> updateDeleteHandler;
boolean canDelete;
BranchesTable() {
@@ -263,90 +232,108 @@
if (Gerrit.getGitwebLink() != null) {
fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
}
+
+ updateDeleteHandler = new ValueChangeHandler<Boolean>() {
+ @Override
+ public void onValueChange(ValueChangeEvent<Boolean> event) {
+ updateDeleteButton();
+ }
+ };
+ }
+
+ Set<String> getCheckedRefs() {
+ Set<String> refs = new HashSet<String>();
+ for (int row = 1; row < table.getRowCount(); row++) {
+ final BranchInfo k = getRowItem(row);
+ if (k != null && table.getWidget(row, 1) instanceof CheckBox
+ && ((CheckBox) table.getWidget(row, 1)).getValue()) {
+ refs.add(k.ref());
+ }
+ }
+ return refs;
+ }
+
+ void setChecked(Set<String> refs) {
+ for (int row = 1; row < table.getRowCount(); row++) {
+ final BranchInfo k = getRowItem(row);
+ if (k != null && refs.contains(k.ref()) &&
+ table.getWidget(row, 1) instanceof CheckBox) {
+ ((CheckBox) table.getWidget(row, 1)).setValue(true);
+ }
+ }
}
void deleteChecked() {
- final SafeHtmlBuilder b = new SafeHtmlBuilder();
+ final Set<String> refs = getCheckedRefs();
+
+ SafeHtmlBuilder b = new SafeHtmlBuilder();
b.openElement("b");
b.append(Gerrit.C.branchDeletionConfirmationMessage());
b.closeElement("b");
b.openElement("p");
- final HashSet<Branch.NameKey> ids = new HashSet<Branch.NameKey>();
- for (int row = 1; row < table.getRowCount(); row++) {
- final Branch k = getRowItem(row);
- if (k != null && table.getWidget(row, 1) instanceof CheckBox
- && ((CheckBox) table.getWidget(row, 1)).getValue()) {
- if (!ids.isEmpty()) {
- b.append(",").br();
- }
- b.append(k.getName());
- ids.add(k.getNameKey());
+ boolean first = true;
+ for (String ref : refs) {
+ if (!first) {
+ b.append(",").br();
}
+ b.append(ref);
+ first = false;
}
b.closeElement("p");
- if (ids.isEmpty()) {
+
+ if (refs.isEmpty()) {
+ updateDeleteButton();
return;
}
+ delBranch.setEnabled(false);
ConfirmationDialog confirmationDialog =
new ConfirmationDialog(Gerrit.C.branchDeletionDialogTitle(),
b.toSafeHtml(), new ConfirmationCallback() {
@Override
public void onOk() {
- deleteBranches(ids);
+ deleteBranches(refs);
+ }
+
+ @Override
+ public void onCancel() {
+ branchTable.updateDeleteButton();
}
});
confirmationDialog.center();
}
- private void deleteBranches(final Set<Branch.NameKey> branchIds) {
- Util.PROJECT_SVC.deleteBranch(getProjectKey(), branchIds,
- new GerritCallback<Set<Branch.NameKey>>() {
- public void onSuccess(final Set<Branch.NameKey> deleted) {
- if (!deleted.isEmpty()) {
- for (int row = 1; row < table.getRowCount();) {
- final Branch k = getRowItem(row);
- if (k != null && deleted.contains(k.getNameKey())) {
- table.removeRow(row);
- } else {
- row++;
- }
+ private void deleteBranches(final Set<String> branches) {
+ ProjectApi.deleteBranches(getProjectKey(), branches,
+ new GerritCallback<VoidResult>() {
+ public void onSuccess(VoidResult result) {
+ for (int row = 1; row < table.getRowCount();) {
+ BranchInfo k = getRowItem(row);
+ if (k != null && branches.contains(k.ref())) {
+ table.removeRow(row);
+ } else {
+ row++;
}
}
+ updateDeleteButton();
+ }
- branchIds.removeAll(deleted);
- if (!branchIds.isEmpty()) {
- final VerticalPanel p = new VerticalPanel();
- final ErrorDialog errorDialog = new ErrorDialog(p);
- final Label l = new Label(Util.C.branchDeletionOpenChanges());
- l.setStyleName(Gerrit.RESOURCES.css().errorDialogText());
- p.add(l);
- for (final Branch.NameKey branch : branchIds) {
- final BranchLink link =
- new BranchLink(branch.getParentKey(), Change.Status.NEW,
- branch.get(), null) {
- @Override
- public void go() {
- errorDialog.hide();
- super.go();
- };
- };
- p.add(link);
- }
- errorDialog.center();
- }
+ @Override
+ public void onFailure(Throwable caught) {
+ refreshBranches();
+ super.onFailure(caught);
}
});
}
- void display(final List<Branch> result) {
+ void display(List<BranchInfo> branches) {
canDelete = false;
while (1 < table.getRowCount())
table.removeRow(table.getRowCount() - 1);
- for (final Branch k : result) {
+ for (final BranchInfo k : branches) {
final int row = table.getRowCount();
table.insertRow(row);
applyDataRowStyle(row);
@@ -354,11 +341,28 @@
}
}
- void populate(final int row, final Branch k) {
+ void insert(BranchInfo info) {
+ Comparator<BranchInfo> c = new Comparator<BranchInfo>() {
+ @Override
+ public int compare(BranchInfo a, BranchInfo b) {
+ return a.ref().compareTo(b.ref());
+ }
+ };
+ int insertPos = getInsertRow(c, info);
+ if (insertPos >= 0) {
+ table.insertRow(insertPos);
+ applyDataRowStyle(insertPos);
+ populate(insertPos, info);
+ }
+ }
+
+ void populate(int row, BranchInfo k) {
final GitwebLink c = Gerrit.getGitwebLink();
- if (k.getCanDelete()) {
- table.setWidget(row, 1, new CheckBox());
+ if (k.canDelete()) {
+ CheckBox sel = new CheckBox();
+ sel.addValueChangeHandler(updateDeleteHandler);
+ table.setWidget(row, 1, sel);
canDelete = true;
} else {
table.setText(row, 1, "");
@@ -366,15 +370,15 @@
table.setText(row, 2, k.getShortName());
- if (k.getRevision() != null) {
- table.setText(row, 3, k.getRevision().get());
+ if (k.revision() != null) {
+ table.setText(row, 3, k.revision());
} else {
table.setText(row, 3, "");
}
if (c != null) {
- table.setWidget(row, 4, new Anchor(c.getLinkName(), false, c.toBranch(k
- .getNameKey())));
+ table.setWidget(row, 4, new Anchor(c.getLinkName(), false,
+ c.toBranch(new Branch.NameKey(getProjectKey(), k.ref()))));
}
final FlexCellFormatter fmt = table.getFlexCellFormatter();
@@ -399,5 +403,20 @@
boolean hasBranchCanDelete() {
return canDelete;
}
+
+ void updateDeleteButton() {
+ boolean on = false;
+ for (int row = 1; row < table.getRowCount(); row++) {
+ Widget w = table.getWidget(row, 1);
+ if (w != null && w instanceof CheckBox) {
+ CheckBox sel = (CheckBox) w;
+ if (sel.getValue()) {
+ on = true;
+ break;
+ }
+ }
+ }
+ delBranch.setEnabled(on);
+ }
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 40921309..cd59078 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -15,14 +15,18 @@
package com.google.gerrit.client.admin;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.access.AccessMap;
+import com.google.gerrit.client.access.ProjectAccessInfo;
import com.google.gerrit.client.download.DownloadPanel;
+import com.google.gerrit.client.projects.ConfigInfo;
+import com.google.gerrit.client.projects.ConfigInfo.InheritedBooleanInfo;
+import com.google.gerrit.client.projects.ProjectApi;
+import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.OnEditEnabler;
import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.data.ProjectDetail;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.client.InheritedBoolean;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
@@ -32,14 +36,17 @@
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
public class ProjectInfoScreen extends ProjectScreen {
- private String projectName;
- private Project project;
+ private boolean isOwner;
+ private Project.NameKey parent;
private LabeledWidgetsGrid grid;
@@ -48,6 +55,8 @@
private ListBox submitType;
private ListBox state;
private ListBox contentMerge;
+ private NpTextBox maxObjectSizeLimit;
+ private Label effectiveMaxObjectSizeLimit;
// Section: Contributor Agreements
private ListBox contributorAgreements;
@@ -60,7 +69,6 @@
public ProjectInfoScreen(final Project.NameKey toShow) {
super(toShow);
- projectName = toShow.get();
}
@Override
@@ -75,7 +83,7 @@
}
});
- add(new ProjectDownloadPanel(projectName, true));
+ add(new ProjectDownloadPanel(getProjectKey().get(), true));
initDescription();
grid = new LabeledWidgetsGrid();
@@ -88,32 +96,49 @@
@Override
protected void onLoad() {
super.onLoad();
- Util.PROJECT_SVC.projectDetail(getProjectKey(),
- new ScreenLoadCallback<ProjectDetail>(this) {
- public void preDisplay(final ProjectDetail result) {
- enableForm(result.canModifyAgreements,
- result.canModifyDescription, result.canModifyMergeType, result.canModifyState);
- saveProject.setVisible(
- result.canModifyAgreements ||
- result.canModifyDescription ||
- result.canModifyMergeType ||
- result.canModifyState);
+
+ Project.NameKey project = getProjectKey();
+ CallbackGroup cbg = new CallbackGroup();
+ AccessMap.get(project,
+ cbg.add(new GerritCallback<ProjectAccessInfo>() {
+ @Override
+ public void onSuccess(ProjectAccessInfo result) {
+ isOwner = result.isOwner();
+ enableForm();
+ saveProject.setVisible(isOwner);
+ }
+ }));
+ ProjectApi.getParent(project,
+ cbg.add(new GerritCallback<Project.NameKey>() {
+ @Override
+ public void onSuccess(Project.NameKey result) {
+ parent = result;
+ }
+ }));
+ ProjectApi.getConfig(project,
+ cbg.addFinal(new ScreenLoadCallback<ConfigInfo>(this) {
+ @Override
+ public void preDisplay(ConfigInfo result) {
display(result);
}
- });
+ }));
+
savedPanel = INFO;
}
- private void enableForm(final boolean canModifyAgreements,
- final boolean canModifyDescription, final boolean canModifyMergeType,
- final boolean canModifyState) {
- submitType.setEnabled(canModifyMergeType);
- state.setEnabled(canModifyState);
- contentMerge.setEnabled(canModifyMergeType);
- descTxt.setEnabled(canModifyDescription);
- contributorAgreements.setEnabled(canModifyAgreements);
- signedOffBy.setEnabled(canModifyAgreements);
- requireChangeID.setEnabled(canModifyMergeType);
+ private void enableForm() {
+ enableForm(isOwner);
+ }
+
+ private void enableForm(boolean isOwner) {
+ submitType.setEnabled(isOwner);
+ state.setEnabled(isOwner);
+ contentMerge.setEnabled(isOwner);
+ descTxt.setEnabled(isOwner);
+ contributorAgreements.setEnabled(isOwner);
+ signedOffBy.setEnabled(isOwner);
+ requireChangeID.setEnabled(isOwner);
+ maxObjectSizeLimit.setEnabled(isOwner);
}
private void initDescription() {
@@ -160,6 +185,15 @@
requireChangeID = newInheritedBooleanBox();
saveEnabler.listenTo(requireChangeID);
grid.addHtml(Util.C.requireChangeID(), requireChangeID);
+
+ maxObjectSizeLimit = new NpTextBox();
+ saveEnabler.listenTo(maxObjectSizeLimit);
+ effectiveMaxObjectSizeLimit = new Label();
+ HorizontalPanel p = new HorizontalPanel();
+ p.setStyleName(Gerrit.RESOURCES.css().maxObjectSizeLimitPanel());
+ p.add(maxObjectSizeLimit);
+ p.add(effectiveMaxObjectSizeLimit);
+ grid.addHtml(Util.C.headingMaxObjectSizeLimit(), p);
}
private static ListBox newInheritedBooleanBox() {
@@ -180,9 +214,9 @@
if (SubmitType.FAST_FORWARD_ONLY.equals(Project.SubmitType
.valueOf(submitType.getValue(submitType.getSelectedIndex())))) {
contentMerge.setEnabled(false);
- final InheritedBoolean inheritedBoolean = new InheritedBoolean();
- inheritedBoolean.setValue(InheritableBoolean.FALSE);
- setBool(contentMerge, inheritedBoolean);
+ InheritedBooleanInfo b = InheritedBooleanInfo.create();
+ b.setConfiguredValue(InheritableBoolean.FALSE);
+ setBool(contentMerge, b);
} else {
contentMerge.setEnabled(submitType.isEnabled());
}
@@ -227,18 +261,18 @@
}
}
- private void setBool(ListBox box, InheritedBoolean inheritedBoolean) {
+ private void setBool(ListBox box, InheritedBooleanInfo inheritedBoolean) {
int inheritedIndex = -1;
for (int i = 0; i < box.getItemCount(); i++) {
if (box.getValue(i).startsWith(InheritableBoolean.INHERIT.name())) {
inheritedIndex = i;
}
- if (box.getValue(i).startsWith(inheritedBoolean.value.name())) {
+ if (box.getValue(i).startsWith(inheritedBoolean.configured_value().name())) {
box.setSelectedIndex(i);
}
}
if (inheritedIndex >= 0) {
- if (project.getParent(Gerrit.getConfig().getWildProject()) == null) {
+ if (parent.equals(Gerrit.getConfig().getWildProject())) {
if (box.getSelectedIndex() == inheritedIndex) {
for (int i = 0; i < box.getItemCount(); i++) {
if (box.getValue(i).equals(InheritableBoolean.FALSE.name())) {
@@ -250,7 +284,7 @@
box.removeItem(inheritedIndex);
} else {
box.setItemText(inheritedIndex, InheritableBoolean.INHERIT.name() + " ("
- + inheritedBoolean.inheritedValue + ")");
+ + inheritedBoolean.inherited_value() + ")");
}
}
}
@@ -267,44 +301,49 @@
return InheritableBoolean.INHERIT;
}
- void display(final ProjectDetail result) {
- project = result.project;
-
- descTxt.setText(project.getDescription());
- setBool(contributorAgreements, result.useContributorAgreements);
- setBool(signedOffBy, result.useSignedOffBy);
- setBool(contentMerge, result.useContentMerge);
- setBool(requireChangeID, result.requireChangeID);
- setSubmitType(project.getSubmitType());
- setState(project.getState());
+ void display(ConfigInfo result) {
+ descTxt.setText(result.description());
+ setBool(contributorAgreements, result.use_contributor_agreements());
+ setBool(signedOffBy, result.use_signed_off_by());
+ setBool(contentMerge, result.use_content_merge());
+ setBool(requireChangeID, result.require_change_id());
+ setSubmitType(result.submit_type());
+ setState(result.state());
+ maxObjectSizeLimit.setText(result.max_object_size_limit().configured_value());
+ if (result.max_object_size_limit().inherited_value() != null) {
+ effectiveMaxObjectSizeLimit.setVisible(true);
+ effectiveMaxObjectSizeLimit.setText(
+ Util.M.effectiveMaxObjectSizeLimit(result.max_object_size_limit().value()));
+ effectiveMaxObjectSizeLimit.setTitle(
+ Util.M.globalMaxObjectSizeLimit(result.max_object_size_limit().inherited_value()));
+ } else {
+ effectiveMaxObjectSizeLimit.setVisible(false);
+ }
saveProject.setEnabled(false);
}
private void doSave() {
- project.setDescription(descTxt.getText().trim());
- project.setUseContributorAgreements(getBool(contributorAgreements));
- project.setUseSignedOffBy(getBool(signedOffBy));
- project.setUseContentMerge(getBool(contentMerge));
- project.setRequireChangeID(getBool(requireChangeID));
- if (submitType.getSelectedIndex() >= 0) {
- project.setSubmitType(Project.SubmitType.valueOf(submitType
- .getValue(submitType.getSelectedIndex())));
- }
- if (state.getSelectedIndex() >= 0) {
- project.setState(Project.State.valueOf(state
- .getValue(state.getSelectedIndex())));
- }
-
- enableForm(false, false, false, false);
-
- Util.PROJECT_SVC.changeProjectSettings(project,
- new GerritCallback<ProjectDetail>() {
- public void onSuccess(final ProjectDetail result) {
- enableForm(result.canModifyAgreements,
- result.canModifyDescription, result.canModifyMergeType, result.canModifyState);
+ enableForm(false);
+ saveProject.setEnabled(false);
+ ProjectApi.setConfig(getProjectKey(), descTxt.getText().trim(),
+ getBool(contributorAgreements), getBool(contentMerge),
+ getBool(signedOffBy), getBool(requireChangeID),
+ maxObjectSizeLimit.getText().trim(),
+ Project.SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
+ Project.State.valueOf(state.getValue(state.getSelectedIndex())),
+ new GerritCallback<ConfigInfo>() {
+ @Override
+ public void onSuccess(ConfigInfo result) {
+ enableForm();
display(result);
}
+
+ @Override
+ public void onFailure(Throwable caught) {
+ enableForm();
+ super.onFailure(caught);
+ }
});
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index ee58420..331afee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -29,6 +29,7 @@
import com.google.gerrit.client.ui.ProjectsTable;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.PageLinks;
+import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.http.client.URL;
@@ -64,10 +65,10 @@
protected void onLoad() {
super.onLoad();
display();
- refresh();
+ refresh(false);
}
- private void refresh() {
+ private void refresh(final boolean open) {
setToken(subname == null || "".equals(subname) ? ADMIN_PROJECTS
: ADMIN_PROJECTS + "?filter=" + URL.encodeQueryString(subname));
ProjectMap.match(subname,
@@ -75,7 +76,12 @@
new GerritCallback<ProjectMap>() {
@Override
public void onSuccess(ProjectMap result) {
- projects.display(result);
+ if (open && result.values().length() > 0) {
+ Gerrit.display(PageLinks.toProject(
+ result.values().get(0).name_key()));
+ } else {
+ projects.display(result);
+ }
}
}));
}
@@ -153,7 +159,7 @@
@Override
public void onKeyUp(KeyUpEvent event) {
subname = filterTxt.getValue();
- refresh();
+ refresh(event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER);
}
});
hp.add(filterTxt);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
new file mode 100644
index 0000000..0fb594d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ActionContext extends JavaScriptObject {
+ static final native void init() /*-{
+ var Gerrit = $wnd.Gerrit;
+ var doc = $wnd.document;
+ var stopPropagation = function (e) {
+ if (e && e.stopPropagation) e.stopPropagation();
+ else $wnd.event.cancelBubble = true;
+ };
+
+ Gerrit.ActionContext = function(u){this._u=u};
+ Gerrit.ActionContext.prototype = {
+ go: Gerrit.go,
+ refresh: Gerrit.refresh,
+
+ br: function(){return doc.createElement('br')},
+ hr: function(){return doc.createElement('hr')},
+ button: function(label, o) {
+ var e = doc.createElement('button');
+ e.appendChild(this.div(doc.createTextNode(label)));
+ if (o && o.onclick) e.onclick = o.onclick;
+ return e;
+ },
+ checkbox: function() {
+ var e = doc.createElement('input');
+ e.type = 'checkbox';
+ return e;
+ },
+ div: function() {
+ var e = doc.createElement('div');
+ for (var i = 0; i < arguments.length; i++)
+ e.appendChild(arguments[i]);
+ return e;
+ },
+ label: function(c,label) {
+ var e = doc.createElement('label');
+ e.appendChild(c);
+ e.appendChild(doc.createTextNode(label));
+ return e;
+ },
+ prependLabel: function(label,c) {
+ var e = doc.createElement('label');
+ e.appendChild(doc.createTextNode(label));
+ e.appendChild(c);
+ return e;
+ },
+ span: function() {
+ var e = doc.createElement('span');
+ for (var i = 0; i < arguments.length; i++)
+ e.appendChild(arguments[i]);
+ return e;
+ },
+ msg: function(label) {
+ var e = doc.createElement('span');
+ e.appendChild(doc.createTextNode(label));
+ return e;
+ },
+ textarea: function(o) {
+ var e = doc.createElement('textarea');
+ e.onkeypress = stopPropagation;
+ if (o && o.rows) e.rows = o.rows;
+ if (o && o.cols) e.cols = o.cols;
+ return e;
+ },
+ textfield: function() {
+ var e = doc.createElement('input');
+ e.type = 'text';
+ e.onkeypress = stopPropagation;
+ return e;
+ },
+
+ popup: function(e){this._p=@com.google.gerrit.client.api.PopupHelper::popup(Lcom/google/gerrit/client/api/ActionContext;Lcom/google/gwt/dom/client/Element;)(this,e)},
+ hide: function() {
+ this._p.@com.google.gerrit.client.api.PopupHelper::hide()();
+ delete this['_p'];
+ },
+
+ call: function(i,b) {
+ var m = this.action.method.toLowerCase();
+ if (m == 'get' || m == 'delete' || i==null) this[m](b);
+ else this[m](i,b);
+ },
+ get: function(b){@com.google.gerrit.client.api.ActionContext::get(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,b)},
+ post: function(i,b){@com.google.gerrit.client.api.ActionContext::post(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,i,b)},
+ put: function(i,b){@com.google.gerrit.client.api.ActionContext::put(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,i,b)},
+ 'delete': function(b){@com.google.gerrit.client.api.ActionContext::delete(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._u,b)},
+ };
+ }-*/;
+
+ static final native ActionContext create(RestApi f)/*-{
+ return new $wnd.Gerrit.ActionContext(f);
+ }-*/;
+
+ final native void set(ActionInfo a) /*-{ this.action=a; }-*/;
+ final native void set(ChangeInfo c) /*-{ this.change=c; }-*/;
+ final native void set(RevisionInfo r) /*-{ this.revision=r; }-*/;
+
+ final native void button(ActionButton b) /*-{ this._b=b; }-*/;
+ final native ActionButton button() /*-{ return this._b; }-*/;
+
+ public final native boolean has_popup() /*-{ return this.hasOwnProperty('_p') }-*/;
+ public final native void hide() /*-{ this.hide(); }-*/;
+
+ protected ActionContext() {
+ }
+
+ static final void get(RestApi api, JavaScriptObject cb) {
+ api.get(wrap(cb));
+ }
+
+ static final void post(RestApi api, JavaScriptObject in, JavaScriptObject cb) {
+ api.post(in, wrap(cb));
+ }
+
+ static final void put(RestApi api, JavaScriptObject in, JavaScriptObject cb) {
+ api.put(in, wrap(cb));
+ }
+
+ static final void delete(RestApi api, JavaScriptObject cb) {
+ api.delete(wrap(cb));
+ }
+
+ private static GerritCallback<JavaScriptObject> wrap(final JavaScriptObject cb) {
+ return new GerritCallback<JavaScriptObject>() {
+ @Override
+ public void onSuccess(JavaScriptObject result) {
+ if (NativeString.is(result)) {
+ NativeString s = result.cast();
+ ApiGlue.invoke(cb, s.asString());
+ } else {
+ ApiGlue.invoke(cb, result);
+ }
+ }
+ };
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
new file mode 100644
index 0000000..a93babe
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
@@ -0,0 +1,125 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.History;
+import com.google.gwt.user.client.Window;
+
+public class ApiGlue {
+ private static String pluginName;
+
+ public static void init() {
+ init0();
+ ActionContext.init();
+ }
+
+ private static native void init0() /*-{
+ var serverUrl = @com.google.gwt.core.client.GWT::getHostPageBaseURL()();
+ var Plugin = function (name){this.name = name};
+ var Gerrit = {
+ getPluginName: @com.google.gerrit.client.api.ApiGlue::getPluginName(),
+ install: function (f) {
+ var p = new Plugin(this.getPluginName());
+ @com.google.gerrit.client.api.ApiGlue::install(Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(f,p);
+ },
+
+ go: @com.google.gerrit.client.api.ApiGlue::go(Ljava/lang/String;),
+ refresh: @com.google.gerrit.client.api.ApiGlue::refresh(),
+
+ change_actions: {},
+ revision_actions: {},
+ onAction: function (t,n,c){this._onAction(this.getPluginName(),t,n,c)},
+ _onAction: function (p,t,n,c) {
+ var i = p+'~'+n;
+ if ('change' == t) this.change_actions[i]=c;
+ else if ('revision' == t) this.revision_actions[i]=c;
+ },
+
+ url: function (d) {
+ if (d && d.length > 0)
+ return serverUrl + (d.charAt(0)=='/' ? d.substring(1) : d);
+ return serverUrl;
+ },
+
+ _api: function(u) {return @com.google.gerrit.client.rpc.RestApi::new(Ljava/lang/String;)(u)},
+ get: function(u,b){@com.google.gerrit.client.api.ActionContext::get(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
+ post: function(u,i,b){@com.google.gerrit.client.api.ActionContext::post(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b)},
+ put: function(u,i,b){@com.google.gerrit.client.api.ActionContext::put(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b)},
+ 'delete': function(u,b){@com.google.gerrit.client.api.ActionContext::delete(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
+ };
+
+ Plugin.prototype = {
+ getPluginName: function(){return this.name},
+ go: @com.google.gerrit.client.api.ApiGlue::go(Ljava/lang/String;),
+ refresh: Gerrit.refresh,
+ onAction: function(t,n,c) {Gerrit._onAction(this.name,t,n,c)},
+
+ url: function (d) {
+ var u = serverUrl + 'plugins/' + this.name + '/';
+ if (d && d.length > 0) u += d.charAt(0)=='/' ? d.substring(1) : d;
+ return u;
+ },
+
+ _api: function(d) {
+ var u = 'plugins/' + this.name + '/';
+ if (d && d.length > 0) u += d.charAt(0)=='/' ? d.substring(1) : d;
+ return @com.google.gerrit.client.rpc.RestApi::new(Ljava/lang/String;)(u);
+ },
+
+ get: function(u,b){@com.google.gerrit.client.api.ActionContext::get(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
+ post: function(u,i,b){@com.google.gerrit.client.api.ActionContext::post(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b)},
+ put: function(u,i,b){@com.google.gerrit.client.api.ActionContext::put(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),i,b)},
+ 'delete': function(u,b){@com.google.gerrit.client.api.ActionContext::delete(Lcom/google/gerrit/client/rpc/RestApi;Lcom/google/gwt/core/client/JavaScriptObject;)(this._api(u),b)},
+ };
+
+ $wnd.Gerrit = Gerrit;
+ }-*/;
+
+ private static void install(JavaScriptObject cb, JavaScriptObject p) {
+ try {
+ pluginName = PluginName.get();
+ invoke(cb, p);
+ } finally {
+ pluginName = null;
+ }
+ }
+
+ private static final String getPluginName() {
+ return pluginName != null ? pluginName : PluginName.get();
+ }
+
+ private static final void go(String urlOrToken) {
+ if (urlOrToken.startsWith("http:")
+ || urlOrToken.startsWith("https:")
+ || urlOrToken.startsWith("//")) {
+ Window.Location.assign(urlOrToken);
+ } else {
+ Gerrit.display(urlOrToken);
+ }
+ }
+
+ private static final void refresh() {
+ Gerrit.display(History.getToken());
+ }
+
+ static final native void invoke(JavaScriptObject f) /*-{ f(); }-*/;
+ static final native void invoke(JavaScriptObject f, JavaScriptObject a) /*-{ f(a); }-*/;
+ static final native void invoke(JavaScriptObject f, String a) /*-{ f(a); }-*/;
+
+ private ApiGlue() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
new file mode 100644
index 0000000..7967147
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ChangeGlue {
+ public static void onAction(
+ ChangeInfo change,
+ ActionInfo action,
+ ActionButton button) {
+ RestApi api = ChangeApi.change(change.legacy_id().get()).view(action.id());
+ JavaScriptObject f = get(action.id());
+ if (f != null) {
+ ActionContext c = ActionContext.create(api);
+ c.set(action);
+ c.set(change);
+ c.button(button);
+ ApiGlue.invoke(f, c);
+ } else {
+ DefaultActions.invoke(change, action, api);
+ }
+ }
+
+ private static final native JavaScriptObject get(String id) /*-{
+ return $wnd.Gerrit.change_actions[id];
+ }-*/;
+
+ private ChangeGlue() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
new file mode 100644
index 0000000..4faed50
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+class DefaultActions {
+ static void invoke(ChangeInfo change, ActionInfo action, RestApi api) {
+ final Change.Id id = change.legacy_id();
+ AsyncCallback<JavaScriptObject> cb = new GerritCallback<JavaScriptObject>() {
+ @Override
+ public void onSuccess(JavaScriptObject msg) {
+ if (NativeString.is(msg)) {
+ NativeString str = (NativeString) msg;
+ if (!str.asString().isEmpty()) {
+ Window.alert(str.asString());
+ }
+ }
+ Gerrit.display(PageLinks.toChange2(id));
+ }
+ };
+ if ("PUT".equalsIgnoreCase(action.method())) {
+ api.put(JavaScriptObject.createObject(), cb);
+ } else if ("DELETE".equalsIgnoreCase(action.method())) {
+ api.delete(cb);
+ } else {
+ api.post(JavaScriptObject.createObject(), cb);
+ }
+ }
+
+ private DefaultActions() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
new file mode 100644
index 0000000..5fc87d5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginName.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptException;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.impl.StackTraceCreator;
+
+/**
+ * Determines the name a plugin has been installed under.
+ *
+ * This implementation guesses the name a plugin runs under by looking at the
+ * JavaScript call stack and identifying the URL of the script file calling
+ * {@code Gerrit.install()}. The simple approach applied here is looking at
+ * the source URLs and extracting the name out of the string, e.g.:
+ * {@code "http://localhost:8080/plugins/{name}/static/foo.js"}.
+ */
+class PluginName {
+ private static final String UNKNOWN = "<unknown>";
+
+ static String get() {
+ return GWT.<PluginName> create(PluginName.class).guessName();
+ }
+
+ String guessName() {
+ JavaScriptException err = makeException();
+ if (hasStack(err)) {
+ return PluginNameMoz.guessName(err);
+ }
+
+ String baseUrl = baseUrl();
+ StackTraceElement[] trace = getTrace(err);
+ for (int i = trace.length - 1; i >= 0; i--) {
+ String u = trace[i].getFileName();
+ if (u != null && u.startsWith(baseUrl)) {
+ int s = u.indexOf('/', baseUrl.length());
+ if (s > 0) {
+ return u.substring(baseUrl.length(), s);
+ }
+ }
+ }
+ return UNKNOWN;
+ }
+
+ private static String baseUrl() {
+ return GWT.getHostPageBaseURL() + "plugins/";
+ }
+
+ private static StackTraceElement[] getTrace(JavaScriptException err) {
+ StackTraceCreator.fillInStackTrace(err);
+ return err.getStackTrace();
+ }
+
+ protected static final native JavaScriptException makeException()
+ /*-{ try { null.a() } catch (e) { return e } }-*/;
+
+ private static final native boolean hasStack(JavaScriptException e)
+ /*-{ return !!e.stack }-*/;
+
+ /** Extracts URL from the stack frame. */
+ static class PluginNameMoz extends PluginName {
+ String guessName() {
+ return guessName(makeException());
+ }
+
+ static String guessName(JavaScriptException e) {
+ String baseUrl = baseUrl();
+ JsArrayString stack = getStack(e);
+ for (int i = stack.length() - 1; i >= 0; i--) {
+ String frame = stack.get(i);
+ int at = frame.indexOf(baseUrl);
+ if (at >= 0) {
+ int s = frame.indexOf('/', at + baseUrl.length());
+ if (s > 0) {
+ return frame.substring(at + baseUrl.length(), s);
+ }
+ }
+ }
+ return UNKNOWN;
+ }
+
+ private static final native JsArrayString getStack(JavaScriptException e)
+ /*-{ return e.stack ? e.stack.split('\n') : [] }-*/;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PopupHelper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PopupHelper.java
new file mode 100644
index 0000000..95d010c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PopupHelper.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.change.Resources;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+class PopupHelper {
+ static PopupHelper popup(ActionContext ctx, Element panel) {
+ PopupHelper helper = new PopupHelper(ctx.button(), panel);
+ helper.show();
+ ctx.button().link(ctx);
+ return helper;
+ }
+
+ private final ActionButton activatingButton;
+ private final FlowPanel panel;
+ private PluginSafePopupPanel popup;
+
+ PopupHelper(ActionButton button, Element child) {
+ activatingButton = button;
+ panel = new FlowPanel();
+ panel.setStyleName(Resources.I.style().popupContent());
+ panel.getElement().appendChild(child);
+ }
+
+ void show() {
+ final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
+ p.setStyleName(Resources.I.style().popup());
+ p.addAutoHidePartner(activatingButton.getElement());
+ p.addCloseHandler(new CloseHandler<PopupPanel>() {
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ activatingButton.unlink();
+ if (popup == p) {
+ popup = null;
+ }
+ }
+ });
+ p.add(panel);
+ p.showRelativeTo(activatingButton);
+ GlobalKey.dialog(p);
+ popup = p;
+ }
+
+ void hide() {
+ if (popup != null) {
+ activatingButton.unlink();
+ popup.hide();
+ popup = null;
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
new file mode 100644
index 0000000..fb489cc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.api;
+
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class RevisionGlue {
+ public static void onAction(
+ ChangeInfo change,
+ RevisionInfo revision,
+ ActionInfo action,
+ ActionButton button) {
+ RestApi api = ChangeApi.revision(
+ change.legacy_id().get(),
+ revision.name())
+ .view(action.id());
+
+ JavaScriptObject f = get(action.id());
+ if (f != null) {
+ ActionContext c = ActionContext.create(api);
+ c.set(action);
+ c.set(change);
+ c.set(revision);
+ c.button(button);
+ ApiGlue.invoke(f, c);
+ } else {
+ DefaultActions.invoke(change, action, api);
+ }
+ }
+
+ private static final native JavaScriptObject get(String id) /*-{
+ return $wnd.Gerrit.revision_actions[id];
+ }-*/;
+
+ private RevisionGlue() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java
new file mode 100644
index 0000000..d46edd3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.user.client.ui.Button;
+
+class AbandonAction extends ActionMessageBox {
+ private final Change.Id id;
+
+ AbandonAction(Button b, Change.Id id) {
+ super(b);
+ this.id = id;
+ }
+
+ void send(String message) {
+ ChangeApi.abandon(id.get(), message, new GerritCallback<ChangeInfo>() {
+ @Override
+ public void onSuccess(ChangeInfo result) {
+ Gerrit.display(PageLinks.toChange2(id));
+ hide();
+ }
+ });
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
new file mode 100644
index 0000000..45f122c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+abstract class ActionMessageBox extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, ActionMessageBox> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ static interface Style extends CssResource {
+ String popup();
+ }
+
+ private final Button activatingButton;
+ private PluginSafePopupPanel popup;
+
+ @UiField Style style;
+ @UiField NpTextArea message;
+ @UiField Button send;
+
+ ActionMessageBox(Button button) {
+ this.activatingButton = button;
+ initWidget(uiBinder.createAndBindUi(this));
+ send.setText(button.getText());
+ }
+
+ abstract void send(String message);
+
+ void show() {
+ if (popup != null) {
+ popup.hide();
+ popup = null;
+ return;
+ }
+
+ final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
+ p.setStyleName(style.popup());
+ p.addAutoHidePartner(activatingButton.getElement());
+ p.addCloseHandler(new CloseHandler<PopupPanel>() {
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ if (popup == p) {
+ popup = null;
+ }
+ }
+ });
+ p.add(this);
+ p.showRelativeTo(activatingButton);
+ GlobalKey.dialog(p);
+ message.setFocus(true);
+ popup = p;
+ }
+
+ void hide() {
+ if (popup != null) {
+ popup.hide();
+ popup = null;
+ }
+ }
+
+ @UiHandler("message")
+ void onMessageKey(KeyPressEvent event) {
+ if ((event.getCharCode() == '\n' || event.getCharCode() == KeyCodes.KEY_ENTER)
+ && event.isControlKeyDown()) {
+ event.preventDefault();
+ event.stopPropagation();
+ onSend(null);
+ }
+ }
+
+ @UiHandler("send")
+ void onSend(ClickEvent e) {
+ send(message.getValue().trim());
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml
new file mode 100644
index 0000000..d639150
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'
+ xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
+ <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+ <ui:style type='com.google.gerrit.client.change.ActionMessageBox.Style'>
+ @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+
+ .popup { background-color: trimColor; }
+ .section {
+ padding: 5px 5px;
+ border-bottom: 1px solid #b8b8b8;
+ }
+ </ui:style>
+ <g:HTMLPanel>
+ <div class='{style.section}'>
+ <c:NpTextArea
+ visibleLines='3'
+ characterWidth='40'
+ ui:field='message'/>
+ </div>
+ <div class='{style.section}'>
+ <g:Button ui:field='send'
+ title='(Shortcut: Ctrl-Enter)'
+ styleName='{res.style.button}'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Send</ui:msg></div>
+ </g:Button>
+ </div>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
new file mode 100644
index 0000000..62a3af0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -0,0 +1,174 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.actions.ActionButton;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+
+import java.util.TreeSet;
+
+class Actions extends Composite {
+ private static final String[] CORE = {
+ "abandon", "restore", "revert", "topic",
+ "cherrypick", "submit", "rebase", "message"};
+
+ interface Binder extends UiBinder<FlowPanel, Actions> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ @UiField Button cherrypick;
+ @UiField Button rebase;
+ @UiField Button revert;
+ @UiField Button submit;
+
+ @UiField Button abandon;
+ private AbandonAction abandonAction;
+
+ @UiField Button restore;
+ private RestoreAction restoreAction;
+
+ private Change.Id changeId;
+ private ChangeInfo changeInfo;
+ private String revision;
+ private String project;
+ private String subject;
+ private String message;
+ private boolean canSubmit;
+
+ Actions() {
+ initWidget(uiBinder.createAndBindUi(this));
+ getElement().setId("change_actions");
+ }
+
+ void display(ChangeInfo info, String revision) {
+ this.revision = revision;
+
+ boolean hasUser = Gerrit.isSignedIn();
+ RevisionInfo revInfo = info.revision(revision);
+ CommitInfo commit = revInfo.commit();
+ changeId = info.legacy_id();
+ project = info.project();
+ subject = commit.subject();
+ message = commit.message();
+ changeInfo = info;
+
+ initChangeActions(info, hasUser);
+ initRevisionActions(info, revInfo, hasUser);
+ }
+
+ private void initChangeActions(ChangeInfo info, boolean hasUser) {
+ NativeMap<ActionInfo> actions = info.has_actions()
+ ? info.actions()
+ : NativeMap.<ActionInfo> create();
+ actions.copyKeysIntoChildren("id");
+
+ abandon.setVisible(hasUser && actions.containsKey("abandon"));
+ restore.setVisible(hasUser && actions.containsKey("restore"));
+ revert.setVisible(hasUser && actions.containsKey("revert"));
+
+ if (hasUser) {
+ for (String id : filterNonCore(actions)) {
+ add(new ActionButton(info, actions.get(id)));
+ }
+ }
+ }
+
+ private void initRevisionActions(ChangeInfo info, RevisionInfo revInfo,
+ boolean hasUser) {
+ NativeMap<ActionInfo> actions = revInfo.has_actions()
+ ? revInfo.actions()
+ : NativeMap.<ActionInfo> create();
+ actions.copyKeysIntoChildren("id");
+
+ cherrypick.setVisible(hasUser && actions.containsKey("cherrypick"));
+ rebase.setVisible(hasUser && actions.containsKey("rebase"));
+ canSubmit = hasUser && actions.containsKey("submit");
+
+ if (hasUser) {
+ for (String id : filterNonCore(actions)) {
+ add(new ActionButton(info, revInfo, actions.get(id)));
+ }
+ }
+ }
+
+ private void add(ActionButton b) {
+ ((FlowPanel) getWidget()).add(b);
+ }
+
+ private static TreeSet<String> filterNonCore(NativeMap<ActionInfo> m) {
+ TreeSet<String> ids = new TreeSet<String>(m.keySet());
+ for (String id : CORE) {
+ ids.remove(id);
+ }
+ return ids;
+ }
+
+ void setSubmitEnabled(boolean ok) {
+ submit.setVisible(ok && canSubmit);
+ }
+
+ boolean isSubmitEnabled() {
+ return submit.isVisible() && submit.isEnabled();
+ }
+
+ @UiHandler("abandon")
+ void onAbandon(ClickEvent e) {
+ if (abandonAction == null) {
+ abandonAction = new AbandonAction(abandon, changeId);
+ }
+ abandonAction.show();
+ }
+
+ @UiHandler("restore")
+ void onRestore(ClickEvent e) {
+ if (restoreAction == null) {
+ restoreAction = new RestoreAction(restore, changeId);
+ }
+ restoreAction.show();
+ }
+
+ @UiHandler("rebase")
+ void onRebase(ClickEvent e) {
+ RebaseAction.call(changeId, revision);
+ }
+
+ @UiHandler("submit")
+ void onSubmit(ClickEvent e) {
+ SubmitAction.call(changeId, revision);
+ }
+
+ @UiHandler("cherrypick")
+ void onCherryPick(ClickEvent e) {
+ CherryPickAction.call(cherrypick, changeInfo, revision, project, message);
+ }
+
+ @UiHandler("revert")
+ void onRevert(ClickEvent e) {
+ RevertAction.call(cherrypick, changeId, revision, project, subject);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
new file mode 100644
index 0000000..699fbcf
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+ <ui:style>
+ #change_actions {
+ padding-top: 2px;
+ padding-bottom: 20px;
+ }
+
+ #change_actions button {
+ margin: 6px 3px 0 0;
+ border-color: rgba(0, 0, 0, 0.1);
+ text-align: center;
+ font-size: 11px;
+ font-weight: bold;
+ border: 1px solid;
+ cursor: pointer;
+ color: #444;
+ background-color: #f5f5f5;
+ background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
+ -webkit-border-radius: 2px;
+ -webkit-box-sizing: content-box;
+ }
+ #change_actions button div {
+ color: #444;
+ height: 10px;
+ min-width: 54px;
+ line-height: 10px;
+ white-space: nowrap;
+ }
+
+ #change_actions button {
+ color: #444;
+ background-color: #f5f5f5;
+ background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
+ }
+ #change_actions button div {color: #444;}
+
+ #change_actions button.red {
+ color: #d14836;
+ background-color: #d14836;
+ background-image: -webkit-linear-gradient(top, #d14836, #d14836);
+ }
+ #change_actions button.red div {color: #fff;}
+
+ #change_actions button.submit {
+ float: right;
+ color: white;
+ background-color: #4d90fe;
+ background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
+ }
+ #change_actions button.submit div {color: #fff;}
+ </ui:style>
+
+ <g:FlowPanel>
+ <g:Button ui:field='cherrypick' styleName=''>
+ <div><ui:msg>Cherry Pick</ui:msg></div>
+ </g:Button>
+ <g:Button ui:field='rebase' styleName=''>
+ <div><ui:msg>Rebase</ui:msg></div>
+ </g:Button>
+ <g:Button ui:field='revert' styleName=''>
+ <div><ui:msg>Revert</ui:msg></div>
+ </g:Button>
+
+ <g:Button ui:field='abandon' styleName='{style.red}'>
+ <div><ui:msg>Abandon</ui:msg></div>
+ </g:Button>
+ <g:Button ui:field='restore' styleName='{style.red}'>
+ <div><ui:msg>Restore</ui:msg></div>
+ </g:Button>
+
+ <g:Button ui:field='submit' styleName='{style.submit}' visible='false'>
+ <div><ui:msg>Submit</ui:msg></div>
+ </g:Button>
+ </g:FlowPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
new file mode 100644
index 0000000..09aef22
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
@@ -0,0 +1,748 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
+import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.changes.ChangeInfo.MergeableInfo;
+import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.changes.ChangeList;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.changes.RevisionInfoCache;
+import com.google.gerrit.client.changes.StarredChanges;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.diff.DiffApi;
+import com.google.gerrit.client.diff.FileInfo;
+import com.google.gerrit.client.projects.ConfigInfoCache;
+import com.google.gerrit.client.projects.ConfigInfoCache.Entry;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.ChangeLink;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.client.ui.UserActivityMonitor;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.AnchorElement;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.ToggleButton;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+import com.google.gwtorm.client.KeyUtil;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ChangeScreen2 extends Screen {
+ interface Binder extends UiBinder<HTMLPanel, ChangeScreen2> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ interface Style extends CssResource {
+ String labelName();
+ String label_user();
+ String label_ok();
+ String label_reject();
+ String label_may();
+ String label_need();
+ String replyBox();
+ String selected();
+ }
+
+ private final Change.Id changeId;
+ private String revision;
+ private ChangeInfo changeInfo;
+ private CommentLinkProcessor commentLinkProcessor;
+
+ private KeyCommandSet keysNavigation;
+ private KeyCommandSet keysAction;
+ private List<HandlerRegistration> handlers = new ArrayList<HandlerRegistration>(4);
+ private UpdateCheckTimer updateCheck;
+ private Timestamp lastDisplayedUpdate;
+ private UpdateAvailableBar updateAvailable;
+ private boolean openReplyBox;
+
+ @UiField HTMLPanel headerLine;
+ @UiField Style style;
+ @UiField ToggleButton star;
+ @UiField Reload reload;
+ @UiField AnchorElement permalink;
+
+ @UiField Element reviewersText;
+ @UiField Element ccText;
+ @UiField Element changeIdText;
+ @UiField Element ownerText;
+ @UiField Element statusText;
+ @UiField Element projectText;
+ @UiField Element branchText;
+ @UiField Element submitActionText;
+ @UiField Element notMergeable;
+ @UiField CopyableLabel idText;
+ @UiField Topic topic;
+ @UiField Element actionText;
+ @UiField Element actionDate;
+
+ @UiField Actions actions;
+ @UiField Labels labels;
+ @UiField CommitBox commit;
+ @UiField RelatedChanges related;
+ @UiField FileTable files;
+ @UiField FlowPanel history;
+
+ @UiField Button revisions;
+ @UiField Button download;
+ @UiField Button reply;
+ @UiField Button expandAll;
+ @UiField Button collapseAll;
+ @UiField Button editMessage;
+ @UiField QuickApprove quickApprove;
+ private ReplyAction replyAction;
+ private EditMessageAction editMessageAction;
+ private RevisionsAction revisionsAction;
+ private DownloadAction downloadAction;
+
+ public ChangeScreen2(Change.Id changeId, String revision, boolean openReplyBox) {
+ this.changeId = changeId;
+ this.revision = revision != null && !revision.isEmpty() ? revision : null;
+ this.openReplyBox = openReplyBox;
+ add(uiBinder.createAndBindUi(this));
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+ loadChangeInfo(true, new GerritCallback<ChangeInfo>() {
+ @Override
+ public void onSuccess(ChangeInfo info) {
+ info.init();
+ loadConfigInfo(info);
+ }
+ });
+ }
+
+ void loadChangeInfo(boolean fg, AsyncCallback<ChangeInfo> cb) {
+ RestApi call = ChangeApi.detail(changeId.get());
+ ChangeList.addOptions(call, EnumSet.of(
+ ListChangesOption.CURRENT_ACTIONS,
+ fg && revision != null
+ ? ListChangesOption.ALL_REVISIONS
+ : ListChangesOption.CURRENT_REVISION));
+ if (!fg) {
+ call.background();
+ }
+ call.get(cb);
+ }
+
+ @Override
+ protected void onUnload() {
+ if (updateAvailable != null) {
+ updateAvailable.hide(true);
+ updateAvailable = null;
+ }
+ if (updateCheck != null) {
+ updateCheck.cancel();
+ updateCheck = null;
+ }
+ for (HandlerRegistration h : handlers) {
+ h.removeHandler();
+ }
+ handlers.clear();
+ super.onUnload();
+ }
+
+ @Override
+ protected void onInitUI() {
+ super.onInitUI();
+ setHeaderVisible(false);
+ Resources.I.style().ensureInjected();
+ star.setVisible(Gerrit.isSignedIn());
+ labels.init(style, statusText);
+
+ keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
+ keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) {
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ Gerrit.displayLastChangeList();
+ }
+ });
+ keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadChange()) {
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ reload.reload();
+ }
+ });
+
+ keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
+ keysAction.add(new KeyCommand(0, 'a', Util.C.keyPublishComments()) {
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ if (Gerrit.isSignedIn()) {
+ onReply(null);
+ } else {
+ Gerrit.doSignIn(getToken());
+ }
+ }
+ });
+ if (Gerrit.isSignedIn()) {
+ keysAction.add(new KeyCommand(0, 's', Util.C.changeTableStar()) {
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ star.setValue(!star.getValue(), true);
+ }
+ });
+ }
+ }
+
+ private void initRevisionsAction(ChangeInfo info, String revision) {
+ revisionsAction = new RevisionsAction(
+ info.legacy_id(), revision,
+ style, headerLine, revisions);
+ }
+
+ private void initDownloadAction(ChangeInfo info, String revision) {
+ downloadAction = new DownloadAction(
+ info.legacy_id(),
+ info.project(),
+ info.revision(revision),
+ style, headerLine, download);
+ }
+
+ private void initEditMessageAction(ChangeInfo info, String revision) {
+ NativeMap<ActionInfo> actions = info.revision(revision).actions();
+ if (actions != null && actions.containsKey("message")) {
+ editMessage.setVisible(true);
+ editMessageAction = new EditMessageAction(
+ info.legacy_id(),
+ revision,
+ info.revision(revision).commit().message(),
+ style,
+ editMessage,
+ reply);
+ keysAction.add(new KeyCommand(0, 'e', Util.C.keyEditMessage()) {
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ editMessageAction.onEdit();
+ }
+ });
+ }
+ }
+
+ @Override
+ public void registerKeys() {
+ super.registerKeys();
+ handlers.add(GlobalKey.add(this, keysNavigation));
+ handlers.add(GlobalKey.add(this, keysAction));
+ files.registerKeys();
+ related.registerKeys();
+ }
+
+ @Override
+ public void onShowView() {
+ super.onShowView();
+
+ related.setMaxHeight(commit.getElement()
+ .getParentElement()
+ .getOffsetHeight());
+
+ if (openReplyBox) {
+ onReply();
+ } else {
+ String prior = Gerrit.getPriorView();
+ if (prior != null && prior.startsWith("/c/")) {
+ scrollToPath(prior.substring(3));
+ }
+ }
+
+ startPoller();
+ }
+
+ private void scrollToPath(String token) {
+ int s = token.indexOf('/');
+ try {
+ if (s < 0 || !changeId.equals(Change.Id.parse(token.substring(0, s)))) {
+ return; // Unrelated URL, do not scroll.
+ }
+ } catch (IllegalArgumentException e) {
+ return;
+ }
+
+ s = token.indexOf('/', s + 1);
+ if (s < 0) {
+ return; // URL does not name a file.
+ }
+
+ int c = token.lastIndexOf(',');
+ if (0 <= c) {
+ token = token.substring(s + 1, c);
+ } else {
+ token = token.substring(s + 1);
+ }
+
+ if (!token.isEmpty()) {
+ files.scrollToPath(KeyUtil.decode(token));
+ }
+ }
+
+ @UiHandler("star")
+ void onToggleStar(ValueChangeEvent<Boolean> e) {
+ StarredChanges.toggleStar(changeId, e.getValue());
+ }
+
+ @UiHandler("download")
+ void onDownload(ClickEvent e) {
+ downloadAction.show();
+ }
+
+ @UiHandler("revisions")
+ void onRevision(ClickEvent e) {
+ revisionsAction.show();
+ }
+
+ @UiHandler("reply")
+ void onReply(ClickEvent e) {
+ onReply();
+ }
+
+ private void onReply() {
+ if (Gerrit.isSignedIn()) {
+ replyAction.onReply();
+ } else {
+ Gerrit.doSignIn(getToken());
+ }
+ }
+
+ @UiHandler("editMessage")
+ void onEditMessage(ClickEvent e) {
+ editMessageAction.onEdit();
+ }
+
+ @UiHandler("expandAll")
+ void onExpandAll(ClickEvent e) {
+ int n = history.getWidgetCount();
+ for (int i = 0; i < n; i++) {
+ ((Message) history.getWidget(i)).setOpen(true);
+ }
+ expandAll.setVisible(false);
+ collapseAll.setVisible(true);
+ }
+
+ @UiHandler("collapseAll")
+ void onCollapseAll(ClickEvent e) {
+ int n = history.getWidgetCount();
+ for (int i = 0; i < n; i++) {
+ ((Message) history.getWidget(i)).setOpen(false);
+ }
+ expandAll.setVisible(true);
+ collapseAll.setVisible(false);
+ }
+
+ private void loadConfigInfo(final ChangeInfo info) {
+ info.revisions().copyKeysIntoChildren("name");
+ final RevisionInfo rev = resolveRevisionToDisplay(info);
+
+ CallbackGroup group = new CallbackGroup();
+ loadDiff(rev, myLastReply(info), group);
+ loadCommit(rev, group);
+ RevisionInfoCache.add(changeId, rev);
+ ConfigInfoCache.add(info);
+ ConfigInfoCache.get(info.project_name_key(),
+ group.add(new ScreenLoadCallback<ConfigInfoCache.Entry>(this) {
+ @Override
+ protected void preDisplay(Entry result) {
+ commentLinkProcessor = result.getCommentLinkProcessor();
+ setTheme(result.getTheme());
+ renderChangeInfo(info);
+ }
+ }));
+ group.done();
+ }
+
+ private static Timestamp myLastReply(ChangeInfo info) {
+ if (Gerrit.isSignedIn() && info.messages() != null) {
+ int self = Gerrit.getUserAccountInfo()._account_id();
+ for (int i = info.messages().length() - 1; i >= 0; i--) {
+ MessageInfo m = info.messages().get(i);
+ if (m.author() != null && m.author()._account_id() == self) {
+ return m.date();
+ }
+ }
+ }
+ return null;
+ }
+
+ private void loadDiff(final RevisionInfo rev, final Timestamp myLastReply,
+ CallbackGroup group) {
+ final List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(rev, group);
+ final List<NativeMap<JsArray<CommentInfo>>> drafts = loadDrafts(rev, group);
+ DiffApi.list(changeId.get(),
+ rev.name(),
+ group.add(new AsyncCallback<NativeMap<FileInfo>>() {
+ @Override
+ public void onSuccess(NativeMap<FileInfo> m) {
+ files.setRevisions(null, new PatchSet.Id(changeId, rev._number()));
+ files.setValue(m, myLastReply, comments.get(0), drafts.get(0));
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ }
+ }));
+
+ if (Gerrit.isSignedIn()) {
+ ChangeApi.revision(changeId.get(), rev.name())
+ .view("files")
+ .addParameterTrue("reviewed")
+ .get(group.add(new AsyncCallback<JsArrayString>() {
+ @Override
+ public void onSuccess(JsArrayString result) {
+ files.markReviewed(result);
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ }
+ }));
+ }
+ }
+
+ private List<NativeMap<JsArray<CommentInfo>>> loadComments(
+ RevisionInfo rev, CallbackGroup group) {
+ final List<NativeMap<JsArray<CommentInfo>>> r =
+ new ArrayList<NativeMap<JsArray<CommentInfo>>>(1);
+ ChangeApi.revision(changeId.get(), rev.name())
+ .view("comments")
+ .get(group.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
+ @Override
+ public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+ r.add(result);
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ }
+ }));
+ return r;
+ }
+
+ private List<NativeMap<JsArray<CommentInfo>>> loadDrafts(
+ RevisionInfo rev, CallbackGroup group) {
+ final List<NativeMap<JsArray<CommentInfo>>> r =
+ new ArrayList<NativeMap<JsArray<CommentInfo>>>(1);
+ if (Gerrit.isSignedIn()) {
+ ChangeApi.revision(changeId.get(), rev.name())
+ .view("drafts")
+ .get(group.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
+ @Override
+ public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+ r.add(result);
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ }
+ }));
+ } else {
+ r.add(NativeMap.<JsArray<CommentInfo>> create());
+ }
+ return r;
+ }
+
+ private void loadCommit(final RevisionInfo rev, CallbackGroup group) {
+ ChangeApi.revision(changeId.get(), rev.name())
+ .view("commit")
+ .get(group.add(new AsyncCallback<CommitInfo>() {
+ @Override
+ public void onSuccess(CommitInfo info) {
+ rev.set_commit(info);
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ }
+ }));
+ }
+
+ private void loadMergeable(final Change.Status status, final boolean canSubmit) {
+ if (Gerrit.getConfig().testChangeMerge()) {
+ ChangeApi.revision(changeId.get(), revision)
+ .view("mergeable")
+ .get(new AsyncCallback<MergeableInfo>() {
+ @Override
+ public void onSuccess(MergeableInfo result) {
+ if (canSubmit) {
+ actions.setSubmitEnabled(result.mergeable());
+ if (status == Change.Status.NEW) {
+ statusText.setInnerText(result.mergeable()
+ ? Util.C.readyToSubmit()
+ : Util.C.mergeConflict());
+ }
+ }
+ setVisible(notMergeable, !result.mergeable());
+ renderSubmitType(result.submit_type());
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ loadSubmitType(status, canSubmit);
+ }
+ });
+ } else {
+ loadSubmitType(status, canSubmit);
+ }
+ }
+
+ private void loadSubmitType(final Change.Status status, final boolean canSubmit) {
+ if (canSubmit) {
+ actions.setSubmitEnabled(true);
+ if (status == Change.Status.NEW) {
+ statusText.setInnerText(Util.C.readyToSubmit());
+ }
+ }
+ ChangeApi.revision(changeId.get(), revision)
+ .view("submit_type")
+ .get(new AsyncCallback<NativeString>() {
+ @Override
+ public void onSuccess(NativeString result) {
+ renderSubmitType(result.asString());
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ }
+ });
+ }
+
+ private RevisionInfo resolveRevisionToDisplay(ChangeInfo info) {
+ if (revision == null) {
+ revision = info.current_revision();
+ } else if (!info.revisions().containsKey(revision)) {
+ JsArray<RevisionInfo> list = info.revisions().values();
+ for (int i = 0; i < list.length(); i++) {
+ RevisionInfo r = list.get(i);
+ if (revision.equals(String.valueOf(r._number()))) {
+ revision = r.name();
+ break;
+ }
+ }
+ }
+ return info.revision(revision);
+ }
+
+ private void renderChangeInfo(ChangeInfo info) {
+ changeInfo = info;
+ lastDisplayedUpdate = info.updated();
+ boolean current = info.status().isOpen()
+ && revision.equals(info.current_revision());
+ boolean canSubmit = labels.set(info, current);
+
+ if (!current && info.status() == Change.Status.NEW) {
+ statusText.setInnerText(Util.C.notCurrent());
+ } else {
+ statusText.setInnerText(Util.toLongString(info.status()));
+ }
+
+ renderOwner(info);
+ renderReviewers(info);
+ renderActionTextDate(info);
+ renderHistory(info);
+ initRevisionsAction(info, revision);
+ initDownloadAction(info, revision);
+ actions.display(info, revision);
+
+ star.setValue(info.starred());
+ permalink.setHref(ChangeLink.permalink(changeId));
+ changeIdText.setInnerText(String.valueOf(info.legacy_id()));
+ projectText.setInnerText(info.project());
+ branchText.setInnerText(info.branch());
+ idText.setText("Change-Id: " + info.change_id());
+ idText.setPreviewText(info.change_id());
+ reload.set(info);
+ topic.set(info, revision);
+ commit.set(commentLinkProcessor, info, revision);
+ related.set(info, revision);
+ quickApprove.set(info, revision);
+
+ if (Gerrit.isSignedIn()) {
+ initEditMessageAction(info, revision);
+ replyAction = new ReplyAction(info, revision, style, reply);
+ if (topic.canEdit()) {
+ keysAction.add(new KeyCommand(0, 't', Util.C.keyEditTopic()) {
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ topic.onEdit();
+ }
+ });
+ }
+ }
+ if (current) {
+ loadMergeable(info.status(), canSubmit);
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(Util.M.changeScreenTitleId(info.id_abbreviated()));
+ if (info.subject() != null) {
+ sb.append(": ");
+ sb.append(info.subject());
+ }
+ setWindowTitle(sb.toString());
+ }
+
+ private void renderReviewers(ChangeInfo info) {
+ // TODO Fix approximation of reviewers and CC list(s).
+ Map<Integer, AccountInfo> r = new HashMap<Integer, AccountInfo>();
+ Map<Integer, AccountInfo> cc = new HashMap<Integer, AccountInfo>();
+ for (LabelInfo label : Natives.asList(info.all_labels().values())) {
+ if (label.all() != null) {
+ for (ApprovalInfo ai : Natives.asList(label.all())) {
+ (ai.value() != 0 ? r : cc).put(ai._account_id(), ai);
+ }
+ }
+ }
+ for (Integer i : r.keySet()) {
+ cc.remove(i);
+ }
+ r.remove(info.owner()._account_id());
+ cc.remove(info.owner()._account_id());
+ reviewersText.setInnerSafeHtml(labels.formatUserList(r.values()));
+ ccText.setInnerSafeHtml(labels.formatUserList(cc.values()));
+ }
+
+ private void renderOwner(ChangeInfo info) {
+ // TODO info card hover
+ String name = info.owner().name() != null
+ ? info.owner().name()
+ : Gerrit.getConfig().getAnonymousCowardName();
+ ownerText.setInnerText(name);
+ ownerText.setTitle(name);
+ }
+
+ private void renderSubmitType(String action) {
+ try {
+ SubmitType type = Project.SubmitType.valueOf(action);
+ submitActionText.setInnerText(
+ com.google.gerrit.client.admin.Util.toLongString(type));
+ } catch (IllegalArgumentException e) {
+ submitActionText.setInnerText(action);
+ }
+ }
+
+ private void renderActionTextDate(ChangeInfo info) {
+ String action;
+ if (info.created().equals(info.updated())) {
+ action = Util.C.changeInfoBlockUploaded();
+ } else {
+ action = Util.C.changeInfoBlockUpdated();
+ }
+ actionText.setInnerText(action);
+ actionDate.setInnerText(FormatUtil.relativeFormat(info.updated()));
+ }
+
+ private void renderHistory(ChangeInfo info) {
+ JsArray<MessageInfo> messages = info.messages();
+ if (messages != null) {
+ for (int i = 0; i < messages.length(); i++) {
+ history.add(new Message(commentLinkProcessor, messages.get(i)));
+ }
+ }
+ }
+
+ void showUpdates(ChangeInfo newInfo) {
+ if (!isAttached() || newInfo.updated().equals(lastDisplayedUpdate)) {
+ return;
+ }
+
+ JsArray<MessageInfo> om = changeInfo.messages();
+ JsArray<MessageInfo> nm = newInfo.messages();
+
+ if (om == null) {
+ om = JsArray.createArray().cast();
+ }
+ if (nm == null) {
+ nm = JsArray.createArray().cast();
+ }
+
+ if (updateAvailable == null) {
+ updateAvailable = new UpdateAvailableBar() {
+ @Override
+ void onShow() {
+ reload.reload();
+ }
+
+ void onIgnore(Timestamp newTime) {
+ lastDisplayedUpdate = newTime;
+ }
+ };
+ updateAvailable.addCloseHandler(new CloseHandler<PopupPanel>() {
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ updateAvailable = null;
+ }
+ });
+ }
+ updateAvailable.set(
+ Natives.asList(nm).subList(om.length(), nm.length()),
+ newInfo.updated());
+ if (!updateAvailable.isShowing()) {
+ updateAvailable.popup();
+ }
+ }
+
+ private void startPoller() {
+ if (Gerrit.isSignedIn() && 0 < Gerrit.getConfig().getChangeUpdateDelay()) {
+ updateCheck = new UpdateCheckTimer(this);
+ updateCheck.schedule();
+ handlers.add(UserActivityMonitor.addValueChangeHandler(updateCheck));
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
new file mode 100644
index 0000000..6cf7b7b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
@@ -0,0 +1,359 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:c='urn:import:com.google.gerrit.client.change'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'
+ xmlns:clippy='urn:import:com.google.gwtexpui.clippy.client'>
+ <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+ <ui:style type='com.google.gerrit.client.change.ChangeScreen2.Style'>
+ @eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
+ @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+
+ @def INFO_WIDTH 450px;
+ @def HEADER_HEIGHT 29px;
+
+ button:disabled {
+ background-color: #999;
+ background-image: -webkit-linear-gradient(top, #999, #999);
+ }
+
+ .headerLine {
+ position: relative;
+ background-color: trimColor;
+ height: HEADER_HEIGHT;
+ }
+
+ .idBlock {
+ position: relative;
+ width: INFO_WIDTH;
+ height: HEADER_HEIGHT;
+ background-color: trimColor;
+ color: textColor;
+ font-family: sans-serif;
+ font-weight: bold;
+ }
+ .star {
+ cursor: pointer;
+ outline: none;
+ position: absolute;
+ left: 5px;
+ top: 5px;
+ }
+ .idLine, .idStatus {
+ line-height: HEADER_HEIGHT;
+ }
+ .idLine {
+ position: absolute;
+ top: 0;
+ left: 29px;
+ width: 245px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ .idStatus {
+ position: absolute;
+ top: 0;
+ right: 26px;
+ }
+ .reload {
+ display: block;
+ position: absolute;
+ top: 7px;
+ right: 5px;
+ cursor: pointer;
+ }
+
+ .headerButtons {
+ position: absolute;
+ top: 0;
+ left: INFO_WIDTH;
+ height: HEADER_HEIGHT;
+ padding-left: 5px;
+ }
+
+ .popdown {
+ position: absolute;
+ top: 2px;
+ right: 0;
+ }
+ .popdown button {
+ cursor: pointer;
+ height: 25px;
+ border: none;
+ border-left: 2px solid #fff;
+ background-color: trimColor;
+ margin: 0;
+ padding-left: 2px;
+ padding-right: 2px;
+ min-width: 100px;
+ }
+ .popdown button div {
+ padding-left: 6px;
+ padding-right: 6px;
+ }
+ .popdown button div:after {
+ content: " \25bc";
+ }
+ .popdown button.selected {
+ font-weight: bold;
+ }
+
+ .headerTable {
+ border-spacing: 0;
+ }
+
+ .headerTable th {
+ width: 60px;
+ color: #444;
+ font-weight: normal;
+ vertical-align: top;
+ text-align: left;
+ padding-right: 5px;
+ }
+
+ .clippy div {
+ float: right;
+ }
+
+ .infoColumn {
+ width: 440px;
+ padding-right: 10px;
+ vertical-align: top;
+ }
+
+ #change_infoTable {
+ border-spacing: 0;
+ width: 100%;
+ margin-left: 2px;
+ margin-right: 5px;
+ }
+
+ .notMergeable {
+ float: right;
+ font-weight: bold;
+ color: red;
+ }
+
+ .commitColumn, .related {
+ padding: 0;
+ vertical-align: top;
+ }
+ .commitColumn {
+ width: 600px;
+ }
+
+ .labels {
+ border-spacing: 0;
+ padding: 0;
+ }
+ .labelName {
+ color: #444;
+ vertical-align: top;
+ text-align: left;
+ padding-right: 5px;
+ white-space: nowrap;
+ }
+ .label_user {white-space: nowrap;}
+ .label_ok {color: #060;}
+ .label_reject {color: #d14836;}
+ .label_need {color: #000;}
+ .label_may {color: #777;}
+
+ .headerButtons button {
+ margin: 6px 3px 0 0;
+ border-color: rgba(0, 0, 0, 0.1);
+ text-align: center;
+ font-size: 11px;
+ font-weight: bold;
+ border: 1px solid;
+ cursor: pointer;
+ color: #444;
+ background-color: #f5f5f5;
+ background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
+ -webkit-border-radius: 2px;
+ -webkit-box-sizing: content-box;
+ }
+ .headerButtons button div {
+ color: #444;
+ height: 10px;
+ min-width: 54px;
+ line-height: 10px;
+ white-space: nowrap;
+ }
+ button.quickApprove {
+ color: #fff;
+ background-color: #4d90fe;
+ background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
+ }
+ button.quickApprove div { color: #fff; }
+
+ .sectionHeader {
+ position: relative;
+ background-color: trimColor;
+ font-weight: bold;
+ color: textColor;
+ height: 18px;
+ padding: 5px 10px;
+ }
+ .sectionHeader .headerButtons {
+ top: 2px;
+ height: 18px;
+ border-left: 1px inset #fff;
+ padding-top: 3px;
+ padding-bottom: 3px;
+ }
+ .sectionHeader button { margin-top: 0; }
+
+ .replyBox {
+ background-color: trimColor;
+ }
+ </ui:style>
+
+ <g:HTMLPanel>
+ <g:HTMLPanel styleName='{style.headerLine}' ui:field='headerLine'>
+ <div class='{style.idBlock}'>
+ <c:StarIcon ui:field='star' styleName='{style.star}'/>
+ <div class='{style.idLine}'>
+ <ui:msg>Change <span ui:field='changeIdText'/> by <span ui:field='ownerText'/></ui:msg>
+ </div>
+ <div ui:field='statusText' class='{style.idStatus}'/>
+ <a ui:field='permalink' class='{style.reload}'>
+ <c:Reload ui:field='reload'
+ title='Reload the change (Shortcut: R)'>
+ <ui:attribute name='title'/>
+ </c:Reload>
+ </a>
+ </div>
+ <div class='{style.headerButtons}'>
+ <g:Button ui:field='reply'
+ styleName=''
+ title='Reply and score (Shortcut: a)'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Reply…</ui:msg></div>
+ </g:Button>
+ <c:QuickApprove ui:field='quickApprove'
+ styleName='{style.quickApprove}'
+ title='Apply score with one click'>
+ <ui:attribute name='title'/>
+ </c:QuickApprove>
+ <g:Button ui:field='editMessage'
+ styleName=''
+ visible='false'
+ title='Edit commit message (Shortcut: e)'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Edit Message</ui:msg></div>
+ </g:Button>
+ </div>
+
+ <g:FlowPanel styleName='{style.popdown}'>
+ <g:Button ui:field='revisions' styleName=''>
+ <div><ui:msg>Revisions</ui:msg></div>
+ </g:Button>
+ <g:Button ui:field='download' styleName=''>
+ <div><ui:msg>Download</ui:msg></div>
+ </g:Button>
+ </g:FlowPanel>
+ </g:HTMLPanel>
+
+ <table class='{style.headerTable}'>
+ <tr>
+ <td class='{style.infoColumn}'>
+ <table id='change_infoTable'>
+ <tr>
+ <th><ui:msg>Reviewers</ui:msg></th>
+ <td ui:field='reviewersText'/>
+ </tr>
+ <tr>
+ <th><ui:msg>CC</ui:msg></th>
+ <td ui:field='ccText'/>
+ </tr>
+ <tr>
+ <th><ui:msg>Project</ui:msg></th>
+ <td ui:field='projectText'/>
+ </tr>
+ <tr>
+ <th><ui:msg>Branch</ui:msg></th>
+ <td ui:field='branchText'/>
+ </tr>
+ <tr>
+ <th><ui:msg>Strategy</ui:msg></th>
+ <td>
+ <span ui:field='submitActionText'/>
+ <div ui:field='notMergeable'
+ class='{style.notMergeable}'
+ style='display: none'
+ aria-hidden='true'>
+ <ui:msg>Cannot Merge</ui:msg>
+ </div>
+ </td>
+ </tr>
+ <tr><td colspan='2'><c:Actions ui:field='actions'/></td></tr>
+ <tr>
+ <th ui:field='actionText'/>
+ <td ui:field='actionDate'/>
+ </tr>
+ <tr>
+ <th><ui:msg>Id</ui:msg></th>
+ <td><clippy:CopyableLabel styleName='{style.clippy}' ui:field='idText'/></td>
+ </tr>
+ <tr>
+ <th><ui:msg>Topic</ui:msg></th>
+ <td><c:Topic ui:field='topic'/></td>
+ </tr>
+ </table>
+ <hr/>
+ <c:Labels ui:field='labels' styleName='{style.labels}'/>
+ </td>
+
+ <td class='{style.commitColumn}'>
+ <c:CommitBox ui:field='commit'/>
+ </td>
+
+ <td class='{style.related}'>
+ <c:RelatedChanges ui:field='related'/>
+ </td>
+ </tr>
+ </table>
+
+ <div class='{style.sectionHeader}'>
+ <ui:msg>Files</ui:msg>
+ </div>
+ <c:FileTable ui:field='files'/>
+
+ <div class='{style.sectionHeader}'>
+ <ui:msg>History</ui:msg>
+ <div class='{style.headerButtons}'>
+ <g:Button ui:field='expandAll'
+ styleName=''
+ title='Expand all messages in the change history'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Expand All</ui:msg></div>
+ </g:Button>
+ <g:Button ui:field='collapseAll'
+ styleName=''
+ visible='false'
+ title='Collapse all messages in the change history'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Collapse All</ui:msg></div>
+ </g:Button>
+ </div>
+ </div>
+ <g:FlowPanel ui:field='history'/>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
new file mode 100644
index 0000000..2c0db0b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CherryPickDialog;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.user.client.ui.Button;
+
+class CherryPickAction {
+ static void call(Button b, final ChangeInfo info, final String revision,
+ String project, final String commitMessage) {
+ // TODO Replace CherryPickDialog with a nicer looking display.
+ b.setEnabled(false);
+ new CherryPickDialog(b, new Project.NameKey(project)) {
+ {
+ sendButton.setText(Util.C.buttonCherryPickChangeSend());
+ if (info.status().isClosed()) {
+ message.setText(Util.M.cherryPickedChangeDefaultMessage(
+ commitMessage.trim(),
+ revision));
+ } else {
+ message.setText(commitMessage.trim());
+ }
+ }
+
+ @Override
+ public void onSend() {
+ ChangeApi.cherrypick(info.legacy_id().get(), revision,
+ getDestinationBranch(),
+ getMessageText(),
+ new GerritCallback<ChangeInfo>() {
+ @Override
+ public void onSuccess(ChangeInfo result) {
+ sent = true;
+ hide();
+ Gerrit.display(PageLinks.toChange2(result.legacy_id()));
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ enableButtons(true);
+ super.onFailure(caught);
+ }
+ });
+ }
+ }.center();
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
new file mode 100644
index 0000000..86b313b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.changes.ChangeInfo.GitPerson;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.AnchorElement;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+class CommitBox extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, CommitBox> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ @UiField Element commitName;
+ @UiField AnchorElement browserLink;
+ @UiField Element authorNameEmail;
+ @UiField Element authorDate;
+ @UiField Element committerNameEmail;
+ @UiField Element committerDate;
+ @UiField Element commitMessageText;
+
+ CommitBox() {
+ initWidget(uiBinder.createAndBindUi(this));
+ }
+
+ void set(CommentLinkProcessor commentLinkProcessor,
+ ChangeInfo change,
+ String revision) {
+ RevisionInfo revInfo = change.revision(revision);
+ CommitInfo commit = revInfo.commit();
+
+ commitName.setInnerText(revision);
+ format(commit.author(), authorNameEmail, authorDate);
+ format(commit.committer(), committerNameEmail, committerDate);
+ commitMessageText.setInnerSafeHtml(commentLinkProcessor.apply(
+ new SafeHtmlBuilder().append(commit.message()).linkify()));
+
+ GitwebLink gw = Gerrit.getGitwebLink();
+ if (gw != null && gw.canLink(revInfo)) {
+ browserLink.setInnerText(gw.getLinkName());
+ browserLink.setHref(gw.toRevision(change.project(), revision));
+ } else {
+ UIObject.setVisible(browserLink, false);
+ }
+ }
+
+ private void format(GitPerson person, Element name, Element date) {
+ name.setInnerText(person.name() + " <" + person.email() + ">");
+ date.setInnerText(FormatUtil.mediumFormat(person.date()));
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
new file mode 100644
index 0000000..c1a6d24
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+ <ui:style>
+ .commitHeader {
+ border-spacing: 0;
+ padding: 0;
+ width: 564px;
+ }
+
+ .commitHeader th { width: 70px; }
+ .commitHeader td { white-space: pre; }
+
+ .commitMessageBox { margin: 2px; }
+ .commitMessage {
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ border: 1px solid white;
+ background-color: white;
+ font-family: monospace;
+ white-space: pre;
+ width: 47em;
+ }
+ </ui:style>
+ <g:HTMLPanel>
+ <table class='{style.commitHeader}'>
+ <tr>
+ <th><ui:msg>Commit</ui:msg></th>
+ <td ui:field='commitName'/>
+ <td><a ui:field='browserLink' href=""/></td>
+ </tr>
+ <tr>
+ <th><ui:msg>Author</ui:msg></th>
+ <td ui:field='authorNameEmail'/>
+ <td ui:field='authorDate'/>
+ </tr>
+ <tr>
+ <th><ui:msg>Committer</ui:msg></th>
+ <td ui:field='committerNameEmail'/>
+ <td ui:field='committerDate'/>
+ </tr>
+ </table>
+
+ <div class='{style.commitMessageBox}'>
+ <div class='{style.commitMessage}' ui:field='commitMessageText'/>
+ </div>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.java
new file mode 100644
index 0000000..ff00e92
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+interface Constants extends com.google.gwt.i18n.client.Constants {
+ String previousChange();
+ String nextChange();
+ String openChange();
+ String reviewedFileTitle();
+
+ String ps();
+ String commit();
+ String date();
+ String author();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.properties
new file mode 100644
index 0000000..bb9450f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Constants.properties
@@ -0,0 +1,9 @@
+previousChange = Previous related change
+nextChange = Next related change
+openChange = Open related change
+reviewedFileTitle = Mark file as reviewed (Shortcut: r)
+
+ps = PS
+commit = Commit
+date = Date
+author = Author
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
new file mode 100644
index 0000000..91ff99c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.changes.ChangeInfo.FetchInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+
+class DownloadAction extends RightSidePopdownAction {
+ private final DownloadBox downloadBox;
+
+ DownloadAction(
+ Change.Id changeId,
+ String project,
+ RevisionInfo revision,
+ ChangeScreen2.Style style,
+ UIObject relativeTo,
+ Widget downloadButton) {
+ super(style, relativeTo, downloadButton);
+ this.downloadBox = new DownloadBox(
+ revision.has_fetch()
+ ? revision.fetch()
+ : NativeMap.<FetchInfo> create(),
+ revision.name(),
+ project,
+ new PatchSet.Id(changeId, revision._number()));
+ }
+
+ Widget getWidget() {
+ return downloadBox;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
new file mode 100644
index 0000000..ae29801
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
@@ -0,0 +1,243 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme.REPO_DOWNLOAD;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountApi;
+import com.google.gerrit.client.changes.ChangeInfo.FetchInfo;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.dom.client.AnchorElement;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+
+class DownloadBox extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, DownloadBox> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ private final NativeMap<FetchInfo> fetch;
+ private final String revision;
+ private final String project;
+ private final PatchSet.Id psId;
+
+ @UiField ListBox scheme;
+ @UiField CopyableLabel checkout;
+ @UiField CopyableLabel cherryPick;
+ @UiField CopyableLabel pull;
+ @UiField AnchorElement patchBase64;
+ @UiField AnchorElement patchZip;
+ @UiField Element repoSection;
+ @UiField CopyableLabel repoDownload;
+
+ DownloadBox(NativeMap<FetchInfo> fetch, String revision,
+ String project, PatchSet.Id psId) {
+ this.fetch = fetch;
+ this.revision = revision;
+ this.project = project;
+ this.psId = psId;
+ initWidget(uiBinder.createAndBindUi(this));
+ }
+
+ @Override
+ protected void onLoad() {
+ if (scheme.getItemCount() == 0) {
+ renderScheme(fetch);
+ }
+ }
+
+ @UiHandler("scheme")
+ void onScheme(ChangeEvent event) {
+ renderCommands();
+
+ if (Gerrit.isSignedIn()) {
+ saveScheme();
+ }
+ }
+
+ private void renderCommands() {
+ FetchInfo info = fetch.get(scheme.getValue(scheme.getSelectedIndex()));
+ checkout(info);
+ cherryPick(info);
+ pull(info);
+ patch(info);
+ repo(info);
+ }
+
+ private void checkout(FetchInfo info) {
+ checkout.setText(
+ "git fetch " + info.url() + " " + info.ref()
+ + " && git checkout FETCH_HEAD");
+ }
+
+ private void cherryPick(FetchInfo info) {
+ cherryPick.setText(
+ "git fetch " + info.url() + " " + info.ref()
+ + " && git cherry-pick FETCH_HEAD");
+ }
+
+ private void pull(FetchInfo info) {
+ pull.setText("git pull " + info.url() + " " + info.ref());
+ }
+
+ private void patch(FetchInfo info) {
+ String id = revision.substring(0, 7);
+ patchBase64.setInnerText(id + ".diff.base64");
+ patchBase64.setHref(new RestApi("/changes/")
+ .id(psId.getParentKey().get())
+ .view("revisions")
+ .id(revision)
+ .view("patch")
+ .addParameterTrue("download")
+ .url());
+
+ patchZip.setInnerText(id + ".diff.zip");
+ patchZip.setHref(new RestApi("/changes/")
+ .id(psId.getParentKey().get())
+ .view("revisions")
+ .id(revision)
+ .view("patch")
+ .addParameterTrue("zip")
+ .url());
+ }
+
+ private void repo(FetchInfo info) {
+ if (Gerrit.getConfig().getDownloadSchemes().contains(REPO_DOWNLOAD)) {
+ UIObject.setVisible(repoSection, true);
+ repoDownload.setText("repo download "
+ + project
+ + " " + psId.getParentKey().get() + "/" + psId.get());
+ }
+ }
+
+ private void renderScheme(NativeMap<FetchInfo> fetch) {
+ for (String id : fetch.keySet()) {
+ FetchInfo info = fetch.get(id);
+ String u = info.url();
+ int css = u.indexOf("://");
+ if (css > 0) {
+ int s = u.indexOf('/', css + 3);
+ if (s > 0) {
+ u = u.substring(0, s + 1);
+ }
+ }
+ scheme.addItem(u, id);
+ }
+ if (scheme.getItemCount() == 1) {
+ scheme.setSelectedIndex(0);
+ scheme.setVisible(false);
+ } else {
+ int select = 0;
+ String find = getUserPreference();
+ if (find != null) {
+ for (int i = 0; i < scheme.getItemCount(); i++) {
+ if (find.equals(scheme.getValue(i))) {
+ select = i;
+ break;
+ }
+ }
+ }
+ scheme.setSelectedIndex(select);
+ }
+ renderCommands();
+ }
+
+ private static String getUserPreference() {
+ if (Gerrit.isSignedIn()) {
+ DownloadScheme pref =
+ Gerrit.getUserAccount().getGeneralPreferences().getDownloadUrl();
+ if (pref != null) {
+ switch (pref) {
+ case ANON_GIT:
+ return "git";
+ case HTTP:
+ case ANON_HTTP:
+ return "http";
+ case SSH:
+ return "ssh";
+ default:
+ return null;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void saveScheme() {
+ DownloadScheme scheme = getSelectedScheme();
+ AccountGeneralPreferences pref =
+ Gerrit.getUserAccount().getGeneralPreferences();
+
+ if (scheme != null && scheme != pref.getDownloadUrl()) {
+ pref.setDownloadUrl(scheme);
+ PreferenceInput in = PreferenceInput.create();
+ in.download_scheme(scheme);
+ AccountApi.self().view("preferences")
+ .post(in, new AsyncCallback<JavaScriptObject>() {
+ @Override
+ public void onSuccess(JavaScriptObject result) {
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ }
+ });
+ }
+ }
+
+ private DownloadScheme getSelectedScheme() {
+ String id = scheme.getValue(scheme.getSelectedIndex());
+ if ("git".equals(id)) {
+ return DownloadScheme.ANON_GIT;
+ } else if ("http".equals(id)) {
+ return DownloadScheme.HTTP;
+ } else if ("ssh".equals(id)) {
+ return DownloadScheme.SSH;
+ }
+ return null;
+ }
+
+ private static class PreferenceInput extends JavaScriptObject {
+ static PreferenceInput create() {
+ return createObject().cast();
+ }
+
+ final void download_scheme(DownloadScheme s) {
+ download_scheme0(s.name());
+ }
+
+ private final native void download_scheme0(String n) /*-{
+ this.download_scheme = n;
+ }-*/;
+
+ protected PreferenceInput() {
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.ui.xml
new file mode 100644
index 0000000..4490982
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.ui.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'
+ xmlns:c='urn:import:com.google.gwtexpui.clippy.client'>
+ <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+ <ui:style>
+ @external .gwt-TextBox;
+
+ .downloadBox {
+ min-width: 580px;
+ margin: 5px;
+ }
+
+ .table {
+ border-spacing: 0;
+ width: 100%;
+ }
+ .table th {
+ text-align: left;
+ font-weight: normal;
+ white-space: nowrap;
+ max-height: 18px;
+ width: 80px;
+ padding-right: 5px;
+ }
+
+ .scheme {
+ float: right;
+ }
+
+ .clippy {
+ font-size: smaller;
+ font-family: monospace;
+ }
+ .clippy span {
+ width: 500px;
+ white-space: nowrap;
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ .clippy .gwt-TextBox {
+ padding: 0;
+ margin: 0;
+ border: 0;
+ max-height: 18px;
+ width: 500px;
+ }
+ .clippy div {
+ float: right;
+ }
+ </ui:style>
+ <g:HTMLPanel styleName='{style.downloadBox}'>
+ <table class='{style.table}'>
+ <tr>
+ <th><ui:msg>Checkout</ui:msg></th>
+ <td><c:CopyableLabel ui:field='checkout' styleName='{style.clippy}'/></td>
+ </tr>
+ <tr>
+ <th><ui:msg>Cherry Pick</ui:msg></th>
+ <td><c:CopyableLabel ui:field='cherryPick' styleName='{style.clippy}'/></td>
+ </tr>
+ <tr>
+ <th><ui:msg>Pull</ui:msg></th>
+ <td><c:CopyableLabel ui:field='pull' styleName='{style.clippy}'/></td>
+ </tr>
+ <tr>
+ <th><ui:msg>Patch File</ui:msg></th>
+ <td><a ui:field='patchZip'/> | <a ui:field='patchBase64'/></td>
+ </tr>
+ <tr ui:field='repoSection' style='display: NONE' aria-hidden='true'>
+ <th><ui:msg>repo</ui:msg></th>
+ <td><c:CopyableLabel ui:field='repoDownload' styleName='{style.clippy}'/></td>
+ </tr>
+ <tr>
+ <td colspan='2'>
+ <g:ListBox ui:field='scheme' styleName='{style.scheme}'/>
+ </td>
+ </tr>
+ </table>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageAction.java
new file mode 100644
index 0000000..a0f7c9d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageAction.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+class EditMessageAction {
+ private final Change.Id changeId;
+ private final String revision;
+ private final String originalMessage;
+ private final ChangeScreen2.Style style;
+ private final Widget editMessageButton;
+ private final Widget replyButton;
+
+ private EditMessageBox editBox;
+ private PopupPanel popup;
+
+ EditMessageAction(
+ Change.Id changeId,
+ String revision,
+ String originalMessage,
+ ChangeScreen2.Style style,
+ Widget editButton,
+ Widget replyButton) {
+ this.changeId = changeId;
+ this.revision = revision;
+ this.originalMessage = originalMessage;
+ this.style = style;
+ this.editMessageButton = editButton;
+ this.replyButton = replyButton;
+ }
+
+ void onEdit() {
+ if (popup != null) {
+ popup.hide();
+ return;
+ }
+
+ if (editBox == null) {
+ editBox = new EditMessageBox(
+ changeId,
+ revision,
+ originalMessage);
+ }
+
+ final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
+ p.setStyleName(style.replyBox());
+ p.addAutoHidePartner(editMessageButton.getElement());
+ p.addCloseHandler(new CloseHandler<PopupPanel>() {
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ if (popup == p) {
+ popup = null;
+ }
+ }
+ });
+ p.add(editBox);
+ p.showRelativeTo(replyButton);
+ GlobalKey.dialog(p);
+ popup = p;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
new file mode 100644
index 0000000..5ddd21b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.TextBoxChangeListener;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+
+class EditMessageBox extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, EditMessageBox> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ private final Change.Id changeId;
+ private final String revision;
+ private String originalMessage;
+
+ @UiField NpTextArea message;
+ @UiField Button save;
+ @UiField Button cancel;
+
+ EditMessageBox(
+ Change.Id changeId,
+ String revision,
+ String msg) {
+ this.changeId = changeId;
+ this.revision = revision;
+ this.originalMessage = msg.trim();
+ initWidget(uiBinder.createAndBindUi(this));
+ new TextBoxChangeListener(message) {
+ public void onTextChanged(String newText) {
+ save.setEnabled(!newText.trim()
+ .equals(originalMessage));
+ }
+ };
+ }
+
+ @Override
+ protected void onLoad() {
+ message.setText(originalMessage);
+ save.setEnabled(false);
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ message.setFocus(true);
+ }});
+ }
+
+ @UiHandler("save")
+ void onSave(ClickEvent e) {
+ ChangeApi.message(changeId.get(), revision, message.getText().trim(),
+ new GerritCallback<JavaScriptObject>() {
+ @Override
+ public void onSuccess(JavaScriptObject msg) {
+ Gerrit.display(PageLinks.toChange2(changeId));
+ hide();
+ };
+ });
+ }
+
+ @UiHandler("cancel")
+ void onCancel(ClickEvent e) {
+ hide();
+ }
+
+ private void hide() {
+ for (Widget w = getParent(); w != null; w = w.getParent()) {
+ if (w instanceof PopupPanel) {
+ ((PopupPanel) w).hide();
+ break;
+ }
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml
new file mode 100644
index 0000000..118409e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.ui.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'
+ xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
+ <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+ <ui:style>
+ .commitMessage {
+ background-color: white;
+ font-family: monospace;
+ }
+ .cancel { float: right; }
+ </ui:style>
+ <g:HTMLPanel>
+ <div class='{res.style.section}'>
+ <c:NpTextArea
+ visibleLines='30'
+ characterWidth='72'
+ styleName='{style.commitMessage}'
+ ui:field='message'/>
+ </div>
+ <div class='{res.style.section}'>
+ <g:Button ui:field='save'
+ title='Create new patch set with updated commit message'
+ styleName='{res.style.button}'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Save</ui:msg></div>
+ </g:Button>
+ <g:Button ui:field='cancel'
+ styleName='{res.style.button}'
+ addStyleNames='{style.cancel}'>
+ <div>Cancel</div>
+ </g:Button>
+ </div>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
new file mode 100644
index 0000000..d081348
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -0,0 +1,612 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.changes.ReviewInfo;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.diff.FileInfo;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.InputElement;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.EventListener;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtexpui.progress.client.ProgressBar;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtorm.client.KeyUtil;
+
+import java.sql.Timestamp;
+
+class FileTable extends FlowPanel {
+ static final FileTableResources R = GWT
+ .create(FileTableResources.class);
+
+ interface FileTableResources extends ClientBundle {
+ @Source("file_table.css")
+ FileTableCss css();
+ }
+
+ interface FileTableCss extends CssResource {
+ String pointer();
+ String reviewed();
+ String pathColumn();
+ String draftColumn();
+ String newColumn();
+ String commentColumn();
+ String deltaColumn1();
+ String deltaColumn2();
+ String commonPrefix();
+ String inserted();
+ String deleted();
+ }
+
+ private static final String REVIEWED;
+ private static final String OPEN;
+ private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
+
+ static {
+ REVIEWED = DOM.createUniqueId().replace('-', '_');
+ OPEN = DOM.createUniqueId().replace('-', '_');
+ init(REVIEWED, OPEN);
+ }
+
+ private static final native void init(String r, String o) /*-{
+ $wnd[r] = $entry(function(e,i) {
+ @com.google.gerrit.client.change.FileTable::onReviewed(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i)
+ });
+ $wnd[o] = $entry(function(e,i) {
+ return @com.google.gerrit.client.change.FileTable::onOpen(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i);
+ });
+ }-*/;
+
+ private static void onReviewed(NativeEvent e, int idx) {
+ MyTable t = getMyTable(e);
+ if (t != null) {
+ t.onReviewed(InputElement.as(Element.as(e.getEventTarget())), idx);
+ }
+ }
+
+ private static boolean onOpen(NativeEvent e, int idx) {
+ if (link.handleAsClick(e.<Event> cast())) {
+ MyTable t = getMyTable(e);
+ if (t != null) {
+ t.onOpenRow(1 + idx);
+ e.preventDefault();
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static MyTable getMyTable(NativeEvent event) {
+ com.google.gwt.user.client.Element e = event.getEventTarget().cast();
+ for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
+ EventListener l = DOM.getEventListener(e);
+ if (l instanceof MyTable) {
+ return (MyTable) l;
+ }
+ }
+ return null;
+ }
+
+ private PatchSet.Id base;
+ private PatchSet.Id curr;
+ private MyTable table;
+ private boolean register;
+ private JsArrayString reviewed;
+ private String scrollToPath;
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+ R.css().ensureInjected();
+ }
+
+ void setRevisions(PatchSet.Id base, PatchSet.Id curr) {
+ this.base = base;
+ this.curr = curr;
+ }
+
+ void setValue(NativeMap<FileInfo> fileMap,
+ Timestamp myLastReply,
+ NativeMap<JsArray<CommentInfo>> comments,
+ NativeMap<JsArray<CommentInfo>> drafts) {
+ JsArray<FileInfo> list = fileMap.values();
+ FileInfo.sortFileInfoByPath(list);
+
+ DisplayCommand cmd = new DisplayCommand(fileMap, list,
+ myLastReply, comments, drafts);
+ if (cmd.execute()) {
+ cmd.showProgressBar();
+ Scheduler.get().scheduleIncremental(cmd);
+ }
+ }
+
+ void markReviewed(JsArrayString reviewed) {
+ if (table != null) {
+ table.markReviewed(reviewed);
+ } else {
+ this.reviewed = reviewed;
+ }
+ }
+
+ void registerKeys() {
+ register = true;
+
+ if (table != null) {
+ table.setRegisterKeys(true);
+ }
+ }
+
+ void scrollToPath(String path) {
+ if (table != null) {
+ table.scrollToPath(path);
+ } else {
+ scrollToPath = path;
+ }
+ }
+
+ private void setTable(MyTable table) {
+ clear();
+ add(table);
+ this.table = table;
+
+ if (register) {
+ table.setRegisterKeys(true);
+ }
+ if (reviewed != null) {
+ table.markReviewed(reviewed);
+ reviewed = null;
+ }
+ if (scrollToPath != null) {
+ table.scrollToPath(scrollToPath);
+ scrollToPath = null;
+ }
+ }
+
+ private String url(FileInfo info) {
+ // TODO(sop): Switch to Dispatcher.toPatchSideBySide.
+ Change.Id c = curr.getParentKey();
+ StringBuilder p = new StringBuilder();
+ p.append("/c/").append(c).append('/');
+ if (base != null) {
+ p.append(base.get()).append("..");
+ }
+ p.append(curr.get()).append('/').append(KeyUtil.encode(info.path()));
+ p.append(info.binary() ? ",unified" : ",cm");
+ return p.toString();
+ }
+
+ private final class MyTable extends NavigationTable<FileInfo> {
+ private final NativeMap<FileInfo> map;
+ private final JsArray<FileInfo> list;
+
+ MyTable(NativeMap<FileInfo> map, JsArray<FileInfo> list) {
+ this.map = map;
+ this.list = list;
+ table.setWidth("");
+
+ keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.patchTablePrev()));
+ keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.patchTableNext()));
+ keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.patchTableOpenDiff()));
+ keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER,
+ Util.C.patchTableOpenDiff()));
+
+ keysAction.add(new KeyCommand(0, 'r', PatchUtil.C.toggleReviewed()) {
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ int row = getCurrentRow();
+ if (1 <= row && row <= MyTable.this.list.length()) {
+ FileInfo info = MyTable.this.list.get(row - 1);
+ InputElement b = getReviewed(info);
+ boolean c = !b.isChecked();
+ setReviewed(info, c);
+ b.setChecked(c);
+ }
+ }
+ });
+
+ setSavePointerId(
+ (base != null ? base.toString() + ".." : "")
+ + curr.toString());
+ }
+
+ void onReviewed(InputElement checkbox, int idx) {
+ setReviewed(list.get(idx), checkbox.isChecked());
+ }
+
+ private void setReviewed(FileInfo info, boolean r) {
+ RestApi api = ChangeApi.revision(curr)
+ .view("files")
+ .id(info.path())
+ .view("reviewed");
+ if (r) {
+ api.put(CallbackGroup.<ReviewInfo>emptyCallback());
+ } else {
+ api.delete(CallbackGroup.<ReviewInfo>emptyCallback());
+ }
+ }
+
+ void markReviewed(JsArrayString reviewed) {
+ for (int i = 0; i < reviewed.length(); i++) {
+ FileInfo info = map.get(reviewed.get(i));
+ if (info != null) {
+ getReviewed(info).setChecked(true);
+ }
+ }
+ }
+
+ private InputElement getReviewed(FileInfo info) {
+ CellFormatter fmt = table.getCellFormatter();
+ Element e = fmt.getElement(1 + info._row(), 1);
+ return InputElement.as(e.getFirstChildElement());
+ }
+
+ void scrollToPath(String path) {
+ FileInfo info = map.get(path);
+ if (info != null) {
+ movePointerTo(1 + info._row(), true);
+ }
+ }
+
+ @Override
+ protected Object getRowItemKey(FileInfo item) {
+ return item.path();
+ }
+
+ @Override
+ protected int findRow(Object id) {
+ FileInfo info = map.get((String) id);
+ return info != null ? 1 + info._row() : -1;
+ }
+
+ @Override
+ protected FileInfo getRowItem(int row) {
+ if (1 <= row && row <= list.length()) {
+ return list.get(row - 1);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onOpenRow(int row) {
+ if (1 <= row && row <= list.length()) {
+ Gerrit.display(url(list.get(row - 1)));
+ }
+ }
+ }
+
+ private final class DisplayCommand implements RepeatingCommand {
+ private final SafeHtmlBuilder sb = new SafeHtmlBuilder();
+ private final MyTable table;
+ private final JsArray<FileInfo> list;
+ private final Timestamp myLastReply;
+ private final NativeMap<JsArray<CommentInfo>> comments;
+ private final NativeMap<JsArray<CommentInfo>> drafts;
+ private final boolean hasUser;
+ private boolean attached;
+ private int row;
+ private double start;
+ private ProgressBar meter;
+ private String lastPath = "";
+
+ private int inserted;
+ private int deleted;
+
+ private DisplayCommand(NativeMap<FileInfo> map,
+ JsArray<FileInfo> list,
+ Timestamp myLastReply,
+ NativeMap<JsArray<CommentInfo>> comments,
+ NativeMap<JsArray<CommentInfo>> drafts) {
+ this.table = new MyTable(map, list);
+ this.list = list;
+ this.myLastReply = myLastReply;
+ this.comments = comments;
+ this.drafts = drafts;
+ this.hasUser = Gerrit.isSignedIn();
+ }
+
+ public boolean execute() {
+ boolean attachedNow = isAttached();
+ if (!attached && attachedNow) {
+ // Remember that we have been attached at least once. If
+ // later we find we aren't attached we should stop running.
+ attached = true;
+ } else if (attached && !attachedNow) {
+ // If the user navigated away, we aren't in the DOM anymore.
+ // Don't continue to render.
+ return false;
+ }
+
+ start = System.currentTimeMillis();
+ if (row == 0) {
+ header(sb);
+ computeInsertedDeleted();
+ }
+ while (row < list.length()) {
+ FileInfo info = list.get(row);
+ info._row(row);
+ render(sb, info);
+ if ((++row % 10) == 0 && longRunning()) {
+ updateMeter();
+ return true;
+ }
+ }
+ footer(sb);
+ table.resetHtml(sb);
+ table.finishDisplay();
+ setTable(table);
+ return false;
+ }
+
+ private void computeInsertedDeleted() {
+ inserted = 0;
+ deleted = 0;
+ for (int i = 0; i < list.length(); i++) {
+ FileInfo info = list.get(i);
+ if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
+ inserted += info.lines_inserted();
+ deleted += info.lines_deleted();
+ }
+ }
+ }
+
+ void showProgressBar() {
+ if (meter == null) {
+ meter = new ProgressBar(Util.M.loadingPatchSet(curr.get()));
+ FileTable.this.clear();
+ FileTable.this.add(meter);
+ }
+ updateMeter();
+ }
+
+ void updateMeter() {
+ if (meter != null) {
+ int n = list.length();
+ meter.setValue((100 * row) / n);
+ }
+ }
+
+ private boolean longRunning() {
+ return System.currentTimeMillis() - start > 200;
+ }
+
+ private void header(SafeHtmlBuilder sb) {
+ sb.openTr();
+ sb.openTh().setStyleName(R.css().pointer()).closeTh();
+ sb.openTh().setStyleName(R.css().reviewed()).closeTh();
+ sb.openTh().append(Util.C.patchTableColumnName()).closeTh();
+ sb.openTh()
+ .setAttribute("colspan", 3)
+ .append(Util.C.patchTableColumnComments())
+ .closeTh();
+ sb.openTh()
+ .setAttribute("colspan", 2)
+ .append(Util.C.patchTableColumnSize())
+ .closeTh();
+ sb.closeTr();
+ }
+
+ private void render(SafeHtmlBuilder sb, FileInfo info) {
+ sb.openTr();
+ sb.openTd().setStyleName(R.css().pointer()).closeTd();
+ columnReviewed(sb, info);
+ columnPath(sb, info);
+ columnComments(sb, info);
+ columnDelta1(sb, info);
+ columnDelta2(sb, info);
+ sb.closeTr();
+ }
+
+ private void columnReviewed(SafeHtmlBuilder sb, FileInfo info) {
+ sb.openTd().setStyleName(R.css().reviewed());
+ if (hasUser) {
+ sb.openElement("input")
+ .setAttribute("title", Resources.C.reviewedFileTitle())
+ .setAttribute("type", "checkbox")
+ .setAttribute("onclick", REVIEWED + "(event," + info._row() + ")")
+ .closeSelf();
+ }
+ sb.closeTd();
+ }
+
+ private void columnPath(SafeHtmlBuilder sb, FileInfo info) {
+ sb.openTd()
+ .setStyleName(R.css().pathColumn())
+ .openAnchor()
+ .setAttribute("href", "#" + url(info))
+ .setAttribute("onclick", OPEN + "(event," + info._row() + ")");
+
+ String path = info.path();
+ if (Patch.COMMIT_MSG.equals(path)) {
+ sb.append(Util.C.commitMessage());
+ } else {
+ int commonPrefixLen = commonPrefix(path);
+ if (commonPrefixLen > 0) {
+ sb.openSpan().setStyleName(R.css().commonPrefix())
+ .append(path.substring(0, commonPrefixLen))
+ .closeSpan();
+ }
+ sb.append(path.substring(commonPrefixLen));
+ lastPath = path;
+ }
+
+ sb.closeAnchor()
+ .closeTd();
+ }
+
+ private int commonPrefix(String path) {
+ for (int n = path.length(); n > 0;) {
+ int s = path.lastIndexOf('/', n);
+ if (s < 0) {
+ return 0;
+ }
+
+ String p = path.substring(0, s + 1);
+ if (lastPath.startsWith(p)) {
+ return s + 1;
+ }
+ n = s - 1;
+ }
+ return 0;
+ }
+
+ private void columnComments(SafeHtmlBuilder sb, FileInfo info) {
+ JsArray<CommentInfo> cList = get(info.path(), comments);
+ JsArray<CommentInfo> dList = get(info.path(), drafts);
+
+ sb.openTd().setStyleName(R.css().draftColumn());
+ if (dList.length() > 0) {
+ sb.append("drafts: ").append(dList.length());
+ }
+ sb.closeTd();
+
+ int cntAll = cList.length();
+ int cntNew = 0;
+ if (myLastReply != null) {
+ for (int i = cntAll - 1; i >= 0; i--) {
+ CommentInfo m = cList.get(i);
+ if (m.updated().compareTo(myLastReply) > 0) {
+ cntNew++;
+ } else {
+ break;
+ }
+ }
+ }
+
+ sb.openTd().setStyleName(R.css().newColumn());
+ if (cntNew > 0) {
+ sb.append("new: ").append(cntNew);
+ }
+ sb.closeTd();
+
+ sb.openTd().setStyleName(R.css().commentColumn());
+ if (cntAll - cntNew > 0) {
+ sb.append("comments: ").append(cntAll - cntNew);
+ }
+ sb.closeTd();
+ }
+
+ private JsArray<CommentInfo> get(String p, NativeMap<JsArray<CommentInfo>> m) {
+ JsArray<CommentInfo> r = m.get(p);
+ if (r == null) {
+ r = JsArray.createArray().cast();
+ }
+ return r;
+ }
+
+ private void columnDelta1(SafeHtmlBuilder sb, FileInfo info) {
+ sb.openTd().setStyleName(R.css().deltaColumn1());
+ if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
+ sb.append(info.lines_inserted() + info.lines_deleted());
+ }
+ sb.closeTd();
+ }
+
+ private void columnDelta2(SafeHtmlBuilder sb, FileInfo info) {
+ sb.openTd().setStyleName(R.css().deltaColumn2());
+ if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()
+ && (info.lines_inserted() != 0 || info.lines_deleted() != 0)) {
+ int w = 80;
+ int t = inserted + deleted;
+ int i = Math.max(5, (int) (((double) w) * info.lines_inserted() / t));
+ int d = Math.max(5, (int) (((double) w) * info.lines_deleted() / t));
+
+ sb.setAttribute(
+ "title",
+ Util.M.patchTableSize_LongModify(info.lines_inserted(),
+ info.lines_deleted()));
+
+ if (0 < info.lines_inserted()) {
+ sb.openDiv()
+ .setStyleName(R.css().inserted())
+ .setAttribute("style", "width:" + i + "px")
+ .closeDiv();
+ }
+ if (0 < info.lines_deleted()) {
+ sb.openDiv()
+ .setStyleName(R.css().deleted())
+ .setAttribute("style", "width:" + d + "px")
+ .closeDiv();
+ }
+ }
+ sb.closeTd();
+ }
+
+ private void footer(SafeHtmlBuilder sb) {
+ sb.openTr();
+ sb.openTh().setStyleName(R.css().pointer()).closeTh();
+ sb.openTh().setStyleName(R.css().reviewed()).closeTh();
+ sb.openTd().closeTd(); // path
+ sb.openTd().setAttribute("colspan", 3).closeTd(); // comments
+
+ // delta1
+ sb.openTh().setStyleName(R.css().deltaColumn1())
+ .append(Util.M.patchTableSize_Modify(inserted, deleted))
+ .closeTh();
+
+ // delta2
+ sb.openTh().setStyleName(R.css().deltaColumn2());
+ int w = 80;
+ int t = inserted + deleted;
+ int i = Math.max(1, (int) (((double) w) * inserted / t));
+ int d = Math.max(1, (int) (((double) w) * deleted / t));
+ if (i + d > w && i > d) {
+ i = w - d;
+ } else if (i + d > w && d > i) {
+ d = w - i;
+ }
+ if (0 < inserted) {
+ sb.openDiv()
+ .setStyleName(R.css().inserted())
+ .setAttribute("style", "width:" + i + "px")
+ .closeDiv();
+ }
+ if (0 < deleted) {
+ sb.openDiv()
+ .setStyleName(R.css().deleted())
+ .setAttribute("style", "width:" + d + "px")
+ .closeDiv();
+ }
+ sb.closeTh();
+
+ sb.closeTr();
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
new file mode 100644
index 0000000..f3ce7fc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
@@ -0,0 +1,215 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Displays a table of label and reviewer scores. */
+class Labels extends Grid {
+ private ChangeScreen2.Style style;
+ private Element statusText;
+
+ void init(ChangeScreen2.Style style, Element statusText) {
+ this.style = style;
+ this.statusText = statusText;
+ }
+
+ boolean set(ChangeInfo info, boolean current) {
+ List<String> names = new ArrayList<String>(info.labels());
+ Collections.sort(names);
+
+ boolean canSubmit = info.status().isOpen();
+ resize(names.size(), 2);
+
+ for (int row = 0; row < names.size(); row++) {
+ String name = names.get(row);
+ LabelInfo label = info.label(name);
+ setText(row, 0, name);
+ if (label.all() != null) {
+ setWidget(row, 1, renderUsers(label));
+ }
+ getCellFormatter().setStyleName(row, 0, style.labelName());
+ getCellFormatter().addStyleName(row, 0, getStyleForLabel(label));
+
+ if (canSubmit && info.status() == Change.Status.NEW) {
+ switch (label.status()) {
+ case NEED:
+ if (current) {
+ statusText.setInnerText("Needs " + name);
+ }
+ canSubmit = false;
+ break;
+ case REJECT:
+ case IMPOSSIBLE:
+ if (current) {
+ statusText.setInnerText("Not " + name);
+ }
+ canSubmit = false;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ return canSubmit;
+ }
+
+ private Widget renderUsers(LabelInfo label) {
+ Map<Integer, List<ApprovalInfo>> m = new HashMap<Integer, List<ApprovalInfo>>(4);
+ int approved = 0, rejected = 0;
+
+ for (ApprovalInfo ai : Natives.asList(label.all())) {
+ if (ai.value() != 0) {
+ List<ApprovalInfo> l = m.get(Integer.valueOf(ai.value()));
+ if (l == null) {
+ l = new ArrayList<ApprovalInfo>(label.all().length());
+ m.put(Integer.valueOf(ai.value()), l);
+ }
+ l.add(ai);
+
+ if (isRejected(label, ai)) {
+ rejected = ai.value();
+ } else if (isApproved(label, ai)) {
+ approved = ai.value();
+ }
+ }
+ }
+
+ SafeHtmlBuilder html = new SafeHtmlBuilder();
+ for (Integer v : sort(m.keySet(), approved, rejected)) {
+ if (!html.isEmpty()) {
+ html.append("; ");
+ }
+
+ String val = LabelValue.formatValue(v.shortValue());
+ html.openSpan();
+ html.setAttribute("title", label.value_text(val));
+ if (v.intValue() == approved) {
+ html.setStyleName(style.label_ok());
+ } else if (v.intValue() == rejected) {
+ html.setStyleName(style.label_reject());
+ }
+ html.append(val).append(" ");
+ html.append(formatUserList(m.get(v)));
+ html.closeSpan();
+ }
+ return html.toBlockWidget();
+ }
+
+ private static List<Integer> sort(Set<Integer> keySet, int a, int b) {
+ List<Integer> r = new ArrayList<Integer>(keySet);
+ Collections.sort(r);
+ if (keySet.contains(a)) {
+ r.remove(Integer.valueOf(a));
+ r.add(0, a);
+ } else if (keySet.contains(b)) {
+ r.remove(Integer.valueOf(b));
+ r.add(0, b);
+ }
+ return r;
+ }
+
+ private static boolean isApproved(LabelInfo label, ApprovalInfo ai) {
+ return label.approved() != null
+ && label.approved()._account_id() == ai._account_id();
+ }
+
+ private static boolean isRejected(LabelInfo label, ApprovalInfo ai) {
+ return label.rejected() != null
+ && label.rejected()._account_id() == ai._account_id();
+ }
+
+ private String getStyleForLabel(LabelInfo label) {
+ switch (label.status()) {
+ case OK:
+ return style.label_ok();
+ case NEED:
+ return style.label_need();
+ case REJECT:
+ case IMPOSSIBLE:
+ return style.label_reject();
+ default:
+ case MAY:
+ return style.label_may();
+ }
+ }
+
+ SafeHtml formatUserList(Collection<? extends AccountInfo> in) {
+ List<AccountInfo> users = new ArrayList<AccountInfo>(in);
+ Collections.sort(users, new Comparator<AccountInfo>() {
+ @Override
+ public int compare(AccountInfo a, AccountInfo b) {
+ String as = name(a);
+ String bs = name(b);
+ if (as.isEmpty()) {
+ return 1;
+ } else if (bs.isEmpty()) {
+ return -1;
+ }
+ return as.compareTo(bs);
+ }
+
+ private String name(AccountInfo a) {
+ if (a.name() != null) {
+ return a.name();
+ } else if (a.email() != null) {
+ return a.email();
+ }
+ return "";
+ }
+ });
+
+ SafeHtmlBuilder html = new SafeHtmlBuilder();
+ Iterator<? extends AccountInfo> itr = users.iterator();
+ while (itr.hasNext()) {
+ AccountInfo ai = itr.next();
+ html.openSpan();
+ html.setStyleName(style.label_user());
+ if (ai.name() != null) {
+ html.append(ai.name());
+ } else if (ai.email() != null) {
+ html.append(ai.email());
+ } else {
+ html.append(ai._account_id());
+ }
+ html.closeSpan();
+ if (itr.hasNext()) {
+ html.append(", ");
+ }
+ }
+ return html;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
new file mode 100644
index 0000000..2d25409
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.AvatarImage;
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+class Message extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, Message> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ static interface Style extends CssResource {
+ String closed();
+ }
+
+ @UiField Style style;
+ @UiField Element name;
+ @UiField Element summary;
+ @UiField Element date;
+ @UiField Element message;
+
+ @UiField(provided = true)
+ AvatarImage avatar;
+
+ Message(CommentLinkProcessor clp, MessageInfo info) {
+ if (info.author() != null) {
+ avatar = new AvatarImage(info.author());
+ avatar.setSize("", "");
+ } else {
+ avatar = new AvatarImage();
+ }
+
+ initWidget(uiBinder.createAndBindUi(this));
+ addDomHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ setOpen(!isOpen());
+ }
+ }, ClickEvent.getType());
+
+ name.setInnerText(authorName(info));
+ date.setInnerText(FormatUtil.shortFormatDayTime(info.date()));
+ if (info.message() != null) {
+ String msg = info.message().trim();
+ summary.setInnerText(msg);
+ message.setInnerSafeHtml(clp.apply(
+ new SafeHtmlBuilder().append(msg).wikify()));
+ }
+ }
+
+ private boolean isOpen() {
+ return UIObject.isVisible(message);
+ }
+
+ void setOpen(boolean open) {
+ UIObject.setVisible(summary, !open);
+ UIObject.setVisible(message, open);
+ if (open) {
+ removeStyleName(style.closed());
+ } else {
+ addStyleName(style.closed());
+ }
+ }
+
+ static String authorName(MessageInfo info) {
+ if (info.author() != null) {
+ if (info.author().name() != null) {
+ return info.author().name();
+ }
+ return Gerrit.getConfig().getAnonymousCowardName();
+ }
+ return Util.C.messageNoAuthor();
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
new file mode 100644
index 0000000..e506340
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:c='urn:import:com.google.gerrit.client'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+ <ui:style type='com.google.gerrit.client.change.Message.Style'>
+ .messageBox {
+ position: relative;
+ width: 1168px;
+ border-left: 1px solid #e3e9ff;
+ border-right: 1px solid #e3e9ff;
+ border-bottom: 1px solid #e3e9ff;
+ -webkit-border-bottom-left-radius: 8px;
+ -webkit-border-bottom-right-radius: 8px;
+ }
+
+ .avatar {
+ position: absolute;
+ width: 26px;
+ height: 26px;
+ }
+ .closed .avatar {
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ }
+
+ .contents {
+ margin-left: 28px;
+ position: relative;
+ }
+
+ .contents p {
+ -webkit-margin-before: 0;
+ -webkit-margin-after: 0.3em;
+ }
+
+ .name {
+ white-space: nowrap;
+ font-weight: bold;
+ }
+ .closed .name {
+ width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-weight: normal;
+ }
+
+ .summary {
+ color: #777;
+ position: absolute;
+ top: 0;
+ left: 120px;
+ width: 915px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .date {
+ white-space: nowrap;
+ position: absolute;
+ top: 0;
+ right: 5px;
+ }
+ </ui:style>
+
+ <g:HTMLPanel
+ styleName='{style.messageBox}'
+ addStyleNames='{style.closed}'>
+ <c:AvatarImage ui:field='avatar' styleName='{style.avatar}'/>
+ <div class='{style.contents}'>
+ <div class='{style.name}' ui:field='name'/>
+ <div ui:field='summary' class='{style.summary}'/>
+ <div class='{style.date}' ui:field='date'/>
+ <div ui:field='message'
+ aria-hidden='true'
+ style='display: NONE'/>
+ </div>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
new file mode 100644
index 0000000..7836de5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
@@ -0,0 +1,119 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.changes.ReviewInput;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+/** Applies a label with one mouse click. */
+class QuickApprove extends Button implements ClickHandler {
+ private Change.Id changeId;
+ private String revision;
+ private ReviewInput input;
+
+ QuickApprove() {
+ addClickHandler(this);
+ }
+
+ void set(ChangeInfo info, String commit) {
+ if (!info.has_permitted_labels() || !info.status().isOpen()) {
+ // Quick approve needs at least one label on an open change.
+ setVisible(false);
+ return;
+ }
+
+ String qName = null;
+ String qValueStr = null;
+ short qValue = 0;
+
+ for (LabelInfo label : Natives.asList(info.all_labels().values())) {
+ if (!info.permitted_labels().containsKey(label.name())) {
+ continue;
+ }
+
+ JsArrayString values = info.permitted_values(label.name());
+ if (values.length() == 0) {
+ continue;
+ }
+
+ switch (label.status()) {
+ case NEED: // Label is required for submit.
+ break;
+
+ case OK: // Label already applied.
+ case MAY: // Label is not required.
+ continue;
+
+ case REJECT: // Submit cannot happen, do not quick approve.
+ case IMPOSSIBLE:
+ setVisible(false);
+ return;
+ }
+
+ String s = values.get(values.length() - 1);
+ short v = LabelInfo.parseValue(s);
+ if (v > 0 && s.equals(label.max_value())) {
+ if (qName != null) {
+ // Quick approve is available for one label only.
+ setVisible(false);
+ return;
+ }
+
+ qName = label.name();
+ qValueStr = s;
+ qValue = v;
+ }
+ }
+
+ if (qName != null) {
+ changeId = info.legacy_id();
+ revision = commit;
+ input = ReviewInput.create();
+ input.label(qName, qValue);
+ setText(qName + qValueStr);
+ } else {
+ setVisible(false);
+ }
+ }
+
+ @Override
+ public void setText(String text) {
+ setHTML(new SafeHtmlBuilder().openDiv().append(text).closeDiv());
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ ChangeApi.revision(changeId.get(), revision)
+ .view("review")
+ .post(input, new GerritCallback<ReviewInput>() {
+ @Override
+ public void onSuccess(ReviewInput result) {
+ Gerrit.display(PageLinks.toChange2(changeId));
+ }
+ });
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
new file mode 100644
index 0000000..0dd6072
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+
+class RebaseAction {
+ static void call(final Change.Id id, String revision) {
+ ChangeApi.rebase(id.get(), revision,
+ new GerritCallback<ChangeInfo>() {
+ public void onSuccess(ChangeInfo result) {
+ Gerrit.display(PageLinks.toChange2(id));
+ }
+ });
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
new file mode 100644
index 0000000..cddf1bf
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
@@ -0,0 +1,345 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.EventListener;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
+import com.google.gwtexpui.progress.client.ProgressBar;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+class RelatedChanges extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, RelatedChanges> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ private static final String OPEN;
+ private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
+
+ static {
+ OPEN = DOM.createUniqueId().replace('-', '_');
+ init(OPEN);
+ }
+
+ private static final native void init(String o) /*-{
+ $wnd[o] = $entry(function(e,i) {
+ return @com.google.gerrit.client.change.RelatedChanges::onOpen(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i);
+ });
+ }-*/;
+
+ private static boolean onOpen(NativeEvent e, int idx) {
+ if (link.handleAsClick(e.<Event> cast())) {
+ MyTable t = getMyTable(e);
+ if (t != null) {
+ t.onOpenRow(idx);
+ e.preventDefault();
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static MyTable getMyTable(NativeEvent event) {
+ com.google.gwt.user.client.Element e = event.getEventTarget().cast();
+ for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
+ EventListener l = DOM.getEventListener(e);
+ if (l instanceof MyTable) {
+ return (MyTable) l;
+ }
+ }
+ return null;
+ }
+
+ interface Style extends CssResource {
+ String subject();
+ }
+
+ private String project;
+ private MyTable table;
+ private boolean register;
+
+ @UiField Style style;
+ @UiField Element header;
+ @UiField Element none;
+ @UiField ScrollPanel scroll;
+ @UiField ProgressBar progress;
+ @UiField Element error;
+
+ RelatedChanges() {
+ initWidget(uiBinder.createAndBindUi(this));
+ }
+
+ void set(ChangeInfo info, final String revision) {
+ if (info.status().isClosed()) {
+ setVisible(false);
+ return;
+ }
+
+ project = info.project();
+
+ ChangeApi.revision(info.legacy_id().get(), revision)
+ .view("related")
+ .get(new AsyncCallback<RelatedInfo>() {
+ @Override
+ public void onSuccess(RelatedInfo result) {
+ render(revision, result.changes());
+ }
+
+ @Override
+ public void onFailure(Throwable err) {
+ progress.setVisible(false);
+ scroll.setVisible(false);
+ UIObject.setVisible(error, true);
+ error.setInnerText(err.getMessage());
+ }
+ });
+ }
+
+ void setMaxHeight(int height) {
+ int h = height - header.getOffsetHeight();
+ scroll.setHeight(h + "px");
+ }
+
+ void registerKeys() {
+ register = true;
+
+ if (table != null) {
+ table.setRegisterKeys(true);
+ }
+ }
+
+ private void render(String revision, JsArray<ChangeAndCommit> list) {
+ if (0 < list.length()) {
+ DisplayCommand cmd = new DisplayCommand(revision, list);
+ if (cmd.execute()) {
+ Scheduler.get().scheduleIncremental(cmd);
+ }
+ } else {
+ progress.setVisible(false);
+ UIObject.setVisible(header, false);
+ UIObject.setVisible(none, true);
+ }
+ }
+
+ private void setTable(MyTable t) {
+ progress.setVisible(false);
+
+ scroll.clear();
+ scroll.add(t);
+ scroll.setVisible(true);
+ table = t;
+
+ if (register) {
+ table.setRegisterKeys(true);
+ }
+ }
+
+ private String url(ChangeAndCommit c) {
+ if (c.has_change_number() && c.has_revision_number()) {
+ PatchSet.Id id = c.patch_set_id();
+ return "#" + PageLinks.toChange2(
+ id.getParentKey(),
+ String.valueOf(id.get()));
+ }
+
+ GitwebLink gw = Gerrit.getGitwebLink();
+ if (gw != null) {
+ return gw.toRevision(project, c.commit().commit());
+ }
+ return null;
+ }
+
+ private class MyTable extends NavigationTable<ChangeAndCommit> {
+ private final JsArray<ChangeAndCommit> list;
+
+ MyTable(JsArray<ChangeAndCommit> list) {
+ this.list = list;
+ table.setWidth("");
+
+ keysNavigation.setName(Gerrit.C.sectionNavigation());
+ keysNavigation.add(new PrevKeyCommand(0, 'K',
+ Resources.C.previousChange()));
+ keysNavigation.add(new NextKeyCommand(0, 'J', Resources.C.nextChange()));
+ keysNavigation.add(new OpenKeyCommand(0, 'O', Resources.C.openChange()));
+ }
+
+ @Override
+ protected Object getRowItemKey(ChangeAndCommit item) {
+ return item.id();
+ }
+
+ @Override
+ protected ChangeAndCommit getRowItem(int row) {
+ if (0 <= row && row <= list.length()) {
+ return list.get(row);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onOpenRow(int row) {
+ if (0 <= row && row <= list.length()) {
+ ChangeAndCommit c = list.get(row);
+ String url = url(c);
+ if (url != null && url.startsWith("#")) {
+ Gerrit.display(url.substring(1));
+ } else if (url != null) {
+ Window.Location.assign(url);
+ }
+ }
+ }
+
+ void selectRow(int select) {
+ movePointerTo(select, true);
+ }
+ }
+
+ private final class DisplayCommand implements RepeatingCommand {
+ private final SafeHtmlBuilder sb = new SafeHtmlBuilder();
+ private final MyTable table;
+ private final String revision;
+ private final JsArray<ChangeAndCommit> list;
+ private boolean attached;
+ private int row;
+ private int select;
+ private double start;
+
+ private DisplayCommand(String revision, JsArray<ChangeAndCommit> list) {
+ this.table = new MyTable(list);
+ this.revision = revision;
+ this.list = list;
+ }
+
+ public boolean execute() {
+ boolean attachedNow = isAttached();
+ if (!attached && attachedNow) {
+ // Remember that we have been attached at least once. If
+ // later we find we aren't attached we should stop running.
+ attached = true;
+ } else if (attached && !attachedNow) {
+ // If the user navigated away, we aren't in the DOM anymore.
+ // Don't continue to render.
+ return false;
+ }
+
+ start = System.currentTimeMillis();
+ while (row < list.length()) {
+ ChangeAndCommit info = list.get(row);
+ if (revision.equals(info.commit().commit())) {
+ select = row;
+ }
+ render(sb, row, info);
+ if ((++row % 10) == 0 && longRunning()) {
+ updateMeter();
+ return true;
+ }
+ }
+ table.resetHtml(sb);
+ setTable(table);
+ table.selectRow(select);
+ return false;
+ }
+
+ private void render(SafeHtmlBuilder sb, int row, ChangeAndCommit info) {
+ sb.openTr();
+ sb.openTd().setStyleName(FileTable.R.css().pointer()).closeTd();
+
+ sb.openTd().addStyleName(style.subject());
+ String url = url(info);
+ if (url != null) {
+ sb.openAnchor().setAttribute("href", url);
+ if (url.startsWith("#")) {
+ sb.setAttribute("onclick", OPEN + "(event," + row + ")");
+ }
+ sb.append(info.commit().subject());
+ sb.closeAnchor();
+ } else {
+ sb.append(info.commit().subject());
+ }
+ sb.closeTd();
+
+ sb.closeTr();
+ }
+
+ private void updateMeter() {
+ progress.setValue((100 * row) / list.length());
+ }
+
+ private boolean longRunning() {
+ return System.currentTimeMillis() - start > 200;
+ }
+ }
+
+ private static class RelatedInfo extends JavaScriptObject {
+ final native JsArray<ChangeAndCommit> changes() /*-{ return this.changes }-*/;
+ protected RelatedInfo() {
+ }
+ }
+
+ private static class ChangeAndCommit extends JavaScriptObject {
+ final native String id() /*-{ return this.change_id }-*/;
+ final native CommitInfo commit() /*-{ return this.commit }-*/;
+
+ final Change.Id legacy_id() {
+ return has_change_number() ? new Change.Id(_change_number()) : null;
+ }
+
+ final PatchSet.Id patch_set_id() {
+ return has_change_number() && has_revision_number()
+ ? new PatchSet.Id(legacy_id(), _revision_number())
+ : null;
+ }
+
+ private final native boolean has_change_number()
+ /*-{ return this.hasOwnProperty('_change_number') }-*/;
+
+ private final native boolean has_revision_number()
+ /*-{ return this.hasOwnProperty('_revision_number') }-*/;
+
+ private final native int _change_number()
+ /*-{ return this._change_number }-*/;
+
+ private final native int _revision_number()
+ /*-{ return this._revision_number }-*/;
+
+ protected ChangeAndCommit() {
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.ui.xml
new file mode 100644
index 0000000..c4a16dc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.ui.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'
+ xmlns:x='urn:import:com.google.gwtexpui.progress.client'>
+ <ui:style type='com.google.gerrit.client.change.RelatedChanges.Style'>
+ .subject {
+ width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ .header, .subject {
+ white-space: nowrap;
+ }
+ </ui:style>
+
+ <g:HTMLPanel>
+ <div ui:field='header' class='{style.header}'
+ title='Same branch changes connected by Git history'>
+ <ui:attribute name='title'/>
+ <ui:msg>Related Changes</ui:msg>
+ </div>
+ <g:ScrollPanel ui:field='scroll' visible='false'/>
+ <x:ProgressBar ui:field='progress'/>
+ <div ui:field='error' aria-hidden='true' style='display: NONE' class='{style.header}'/>
+ <div ui:field='none' aria-hidden='true' style='display: NONE' class='{style.header}'>
+ <ui:msg>No Related Changes</ui:msg>
+ </div>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reload.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reload.java
new file mode 100644
index 0000000..bd0c0e2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reload.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOutHandler;
+import com.google.gwt.event.dom.client.MouseOverEvent;
+import com.google.gwt.event.dom.client.MouseOverHandler;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
+
+class Reload extends Image implements ClickHandler,
+ MouseOverHandler, MouseOutHandler {
+ private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
+ private Change.Id changeId;
+ private boolean in;
+
+ Reload() {
+ setResource(Resources.I.reload_black());
+ addClickHandler(this);
+ addMouseOverHandler(this);
+ addMouseOutHandler(this);
+ }
+
+ void set(ChangeInfo info) {
+ changeId = info.legacy_id();
+ }
+
+ void reload() {
+ Gerrit.display(PageLinks.toChange2(changeId));
+ }
+
+ @Override
+ public void onMouseOver(MouseOverEvent event) {
+ if (!in) {
+ in = true;
+ setResource(Resources.I.reload_white());
+ }
+ }
+
+ @Override
+ public void onMouseOut(MouseOutEvent event) {
+ if (in) {
+ in = false;
+ setResource(Resources.I.reload_black());
+ }
+ }
+
+ @Override
+ public void onClick(ClickEvent e) {
+ if (link.handleAsClick(e.getNativeEvent().<Event> cast())) {
+ e.preventDefault();
+ e.stopPropagation();
+ reload();
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
new file mode 100644
index 0000000..d320df4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+class ReplyAction {
+ private final PatchSet.Id psId;
+ private final String revision;
+ private final ChangeScreen2.Style style;
+ private final Widget replyButton;
+
+ private NativeMap<LabelInfo> allLabels;
+ private NativeMap<JsArrayString> permittedLabels;
+
+ private ReplyBox replyBox;
+ private PopupPanel popup;
+
+ ReplyAction(
+ ChangeInfo info,
+ String revision,
+ ChangeScreen2.Style style,
+ Widget replyButton) {
+ this.psId = new PatchSet.Id(
+ info.legacy_id(),
+ info.revisions().get(revision)._number());
+ this.revision = revision;
+ this.style = style;
+ this.replyButton = replyButton;
+
+ boolean current = revision.equals(info.current_revision());
+ allLabels = info.all_labels();
+ permittedLabels = current && info.has_permitted_labels()
+ ? info.permitted_labels()
+ : NativeMap.<JsArrayString> create();
+ }
+
+ void onReply() {
+ if (popup != null) {
+ popup.hide();
+ return;
+ }
+
+ if (replyBox == null) {
+ replyBox = new ReplyBox(
+ psId,
+ revision,
+ allLabels,
+ permittedLabels);
+ allLabels = null;
+ permittedLabels = null;
+ }
+
+ final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
+ p.setStyleName(style.replyBox());
+ p.addAutoHidePartner(replyButton.getElement());
+ p.addCloseHandler(new CloseHandler<PopupPanel>() {
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ if (popup == p) {
+ popup = null;
+ }
+ }
+ });
+ p.add(replyBox);
+ p.showRelativeTo(replyButton);
+ GlobalKey.dialog(p);
+ popup = p;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
new file mode 100644
index 0000000..01110de
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -0,0 +1,279 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.changes.ReviewInput;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.RadioButton;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+class ReplyBox extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, ReplyBox> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ interface Styles extends CssResource {
+ String label_name();
+ String label_value();
+ }
+
+ private final PatchSet.Id psId;
+ private final String revision;
+ private ReviewInput in = ReviewInput.create();
+ private List<Runnable> lgtm;
+
+ @UiField Styles style;
+ @UiField NpTextArea message;
+ @UiField Element labelsParent;
+ @UiField Grid labelsTable;
+ @UiField Button send;
+ @UiField CheckBox email;
+
+ ReplyBox(
+ PatchSet.Id psId,
+ String revision,
+ NativeMap<LabelInfo> all,
+ NativeMap<JsArrayString> permitted) {
+ this.psId = psId;
+ this.revision = revision;
+ initWidget(uiBinder.createAndBindUi(this));
+
+ List<String> names = new ArrayList<String>(permitted.keySet());
+ if (names.isEmpty()) {
+ UIObject.setVisible(labelsParent, false);
+ } else {
+ Collections.sort(names);
+ lgtm = new ArrayList<Runnable>(names.size());
+ renderLabels(names, all, permitted);
+ }
+ }
+
+ @Override
+ protected void onLoad() {
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ message.setFocus(true);
+ }});
+ }
+
+ @UiHandler("message")
+ void onMessageKey(KeyPressEvent event) {
+ if ((event.getCharCode() == '\n' || event.getCharCode() == KeyCodes.KEY_ENTER)
+ && event.isControlKeyDown()) {
+ event.preventDefault();
+ event.stopPropagation();
+ onSend(null);
+ } else if (lgtm != null
+ && event.getCharCode() == 'M'
+ && message.getValue().equals("LGT")) {
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ if (message.getValue().startsWith("LGTM")) {
+ for (Runnable r : lgtm) {
+ r.run();
+ }
+ }
+ }
+ });
+ }
+ }
+
+ @UiHandler("email")
+ void onEmail(ValueChangeEvent<Boolean> e) {
+ if (e.getValue()) {
+ in.notify(ReviewInput.NotifyHandling.ALL);
+ } else {
+ in.notify(ReviewInput.NotifyHandling.NONE);
+ }
+ }
+
+ @UiHandler("send")
+ void onSend(ClickEvent e) {
+ in.message(message.getText().trim());
+ ChangeApi.revision(psId.getParentKey().get(), revision)
+ .view("review")
+ .post(in, new GerritCallback<ReviewInput>() {
+ @Override
+ public void onSuccess(ReviewInput result) {
+ Gerrit.display(PageLinks.toChange2(
+ psId.getParentKey(),
+ String.valueOf(psId.get())));
+ }
+ });
+ hide();
+ }
+
+ private void hide() {
+ for (Widget w = getParent(); w != null; w = w.getParent()) {
+ if (w instanceof PopupPanel) {
+ ((PopupPanel) w).hide();
+ break;
+ }
+ }
+ }
+
+ private void renderLabels(
+ List<String> names,
+ NativeMap<LabelInfo> all,
+ NativeMap<JsArrayString> permitted) {
+ TreeSet<Short> values = new TreeSet<Short>();
+ for (String id : names) {
+ JsArrayString p = permitted.get(id);
+ if (p != null) {
+ for (int i = 0; i < p.length(); i++) {
+ values.add(LabelInfo.parseValue(p.get(i)));
+ }
+ }
+ }
+ List<Short> columns = new ArrayList<Short>(values);
+
+ labelsTable.resize(1 + permitted.size(), 1 + values.size());
+ for (int c = 0; c < columns.size(); c++) {
+ labelsTable.setText(0, 1 + c, LabelValue.formatValue(columns.get(c)));
+ labelsTable.getCellFormatter().setStyleName(0, 1 + c, style.label_value());
+ }
+
+ List<String> checkboxes = new ArrayList<String>(permitted.size());
+ int row = 1;
+ for (String id : names) {
+ Set<Short> vals = all.get(id).value_set();
+ if (isCheckBox(vals)) {
+ checkboxes.add(id);
+ } else {
+ renderRadio(row++, id, columns, vals, all.get(id));
+ }
+ }
+ for (String id : checkboxes) {
+ renderCheckBox(row++, id, all.get(id));
+ }
+ }
+
+ private void renderRadio(int row, final String id,
+ List<Short> columns,
+ Set<Short> values,
+ LabelInfo info) {
+ labelsTable.setText(row, 0, id);
+ labelsTable.getCellFormatter().setStyleName(row, 0, style.label_name());
+
+ ApprovalInfo self = Gerrit.isSignedIn()
+ ? info.for_user(Gerrit.getUserAccount().getId().get())
+ : null;
+
+ final List<RadioButton> group = new ArrayList<RadioButton>(values.size());
+ for (int i = 0; i < columns.size(); i++) {
+ final Short v = columns.get(i);
+ if (values.contains(v)) {
+ RadioButton b = new RadioButton(id);
+ b.setTitle(info.value_text(LabelValue.formatValue(v)));
+ if ((self != null && v == self.value()) || (self == null && v == 0)) {
+ b.setValue(true);
+ }
+ b.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+ @Override
+ public void onValueChange(ValueChangeEvent<Boolean> event) {
+ if (event.getValue()) {
+ in.label(id, v);
+ }
+ }
+ });
+ group.add(b);
+ labelsTable.setWidget(row, 1 + i, b);
+ }
+ }
+
+ if (!group.isEmpty()) {
+ lgtm.add(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < group.size() - 1; i++) {
+ group.get(i).setValue(false, false);
+ }
+ group.get(group.size() - 1).setValue(true, true);
+ }
+ });
+ }
+ }
+
+ private void renderCheckBox(int row, final String id, LabelInfo info) {
+ ApprovalInfo self = Gerrit.isSignedIn()
+ ? info.for_user(Gerrit.getUserAccount().getId().get())
+ : null;
+
+ final CheckBox b = new CheckBox();
+ b.setText(id);
+ b.setTitle(info.value_text("+1"));
+ if (self != null && self.value() == 1) {
+ b.setValue(true);
+ }
+ b.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+ @Override
+ public void onValueChange(ValueChangeEvent<Boolean> event) {
+ in.label(id, event.getValue() ? (short) 1 : (short) 0);
+ }
+ });
+ b.setStyleName(style.label_name());
+ labelsTable.setWidget(row, 0, b);
+
+ lgtm.add(new Runnable() {
+ @Override
+ public void run() {
+ b.setValue(true, true);
+ }
+ });
+ }
+
+ private static boolean isCheckBox(Set<Short> values) {
+ return values.size() == 2
+ && values.contains((short) 0)
+ && values.contains((short) 1);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
new file mode 100644
index 0000000..3da3c9e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'
+ xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
+ <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+ <ui:style type='com.google.gerrit.client.change.ReplyBox.Styles'>
+ .replyBox {
+ max-height: 260px;
+ }
+
+ .label_name {
+ font-weight: bold;
+ text-align: left;
+ }
+ .label_name input { margin-left: 0; }
+
+ .label_value {
+ text-align: center;
+ }
+ .email {
+ display: inline-block;
+ margin-left: 2em;
+ }
+ </ui:style>
+ <g:HTMLPanel styleName='{style.replyBox}'>
+ <div class='{res.style.section}'>
+ <c:NpTextArea
+ visibleLines='5'
+ characterWidth='70'
+ ui:field='message'/>
+ </div>
+ <div class='{res.style.section}' ui:field='labelsParent'>
+ <g:Grid ui:field='labelsTable'/>
+ </div>
+ <div class='{res.style.section}'>
+ <g:Button ui:field='send'
+ title='Send reply (Shortcut: Ctrl-Enter)'
+ styleName='{res.style.button}'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Send</ui:msg></div>
+ </g:Button>
+
+ <div class='{style.email}'>
+ <ui:msg>and <g:CheckBox ui:field='email' value='true'>send email</g:CheckBox></ui:msg>
+ </div>
+ </div>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
new file mode 100644
index 0000000..76cbc6a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Resources.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.resources.client.ImageResource;
+
+public interface Resources extends ClientBundle {
+ public static final Resources I = GWT.create(Resources.class);
+ static final Constants C = GWT.create(Constants.class);
+
+ @Source("star_open.png") ImageResource star_open();
+ @Source("star_filled.png") ImageResource star_filled();
+ @Source("reload_black.png") ImageResource reload_black();
+ @Source("reload_white.png") ImageResource reload_white();
+ @Source("common.css") Style style();
+
+ public interface Style extends CssResource {
+ String button();
+ String popup();
+ String popupContent();
+ String section();
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java
new file mode 100644
index 0000000..82f9b0c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.user.client.ui.Button;
+
+class RestoreAction extends ActionMessageBox {
+ private final Change.Id id;
+
+ RestoreAction(Button b, Change.Id id) {
+ super(b);
+ this.id = id;
+ }
+
+ void send(String message) {
+ ChangeApi.restore(id.get(), message, new GerritCallback<ChangeInfo>() {
+ @Override
+ public void onSuccess(ChangeInfo result) {
+ Gerrit.display(PageLinks.toChange2(id));
+ hide();
+ }
+ });
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
new file mode 100644
index 0000000..3190d79
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.ActionDialog;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.user.client.ui.Button;
+
+class RevertAction {
+ static void call(Button b, final Change.Id id, final String revision,
+ String project, final String commitSubject) {
+ // TODO Replace ActionDialog with a nicer looking display.
+ b.setEnabled(false);
+ new ActionDialog(b, false,
+ Util.C.revertChangeTitle(),
+ Util.C.headingRevertMessage()) {
+ {
+ sendButton.setText(Util.C.buttonRevertChangeSend());
+ message.setText(Util.M.revertChangeDefaultMessage(
+ commitSubject, revision));
+ }
+
+ @Override
+ public void onSend() {
+ ChangeApi.revert(id.get(),
+ getMessageText(), new GerritCallback<ChangeInfo>() {
+ @Override
+ public void onSuccess(ChangeInfo result) {
+ sent = true;
+ hide();
+ Gerrit.display(PageLinks.toChange2(result.legacy_id()));
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ enableButtons(true);
+ super.onFailure(caught);
+ }
+ });
+ }
+ }.center();
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsAction.java
new file mode 100644
index 0000000..d66127b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsAction.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+
+class RevisionsAction extends RightSidePopdownAction {
+ private final RevisionsBox revisionBox;
+
+ RevisionsAction(
+ Change.Id changeId,
+ String revision,
+ ChangeScreen2.Style style,
+ UIObject relativeTo,
+ Widget downloadButton) {
+ super(style, relativeTo, downloadButton);
+ this.revisionBox = new RevisionsBox(changeId, revision);
+ }
+
+ Widget getWidget() {
+ return revisionBox;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.java
new file mode 100644
index 0000000..c0b2112
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.java
@@ -0,0 +1,219 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.changes.ChangeList;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.client.ui.FancyFlexTableImpl;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.EventListener;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import java.util.Collections;
+import java.util.EnumSet;
+
+class RevisionsBox extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, RevisionsBox> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ private static final String OPEN;
+ private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
+
+ static {
+ OPEN = DOM.createUniqueId().replace('-', '_');
+ init(OPEN);
+ }
+
+ private static final native void init(String o) /*-{
+ $wnd[o] = $entry(function(e,i) {
+ return @com.google.gerrit.client.change.RevisionsBox::onOpen(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i);
+ });
+ }-*/;
+
+ private static boolean onOpen(NativeEvent e, int idx) {
+ if (link.handleAsClick(e.<Event> cast())) {
+ RevisionsBox t = getRevisionBox(e);
+ if (t != null) {
+ t.onOpenRow(idx);
+ e.preventDefault();
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static RevisionsBox getRevisionBox(NativeEvent event) {
+ com.google.gwt.user.client.Element e = event.getEventTarget().cast();
+ for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
+ EventListener l = DOM.getEventListener(e);
+ if (l instanceof RevisionsBox) {
+ return (RevisionsBox) l;
+ }
+ }
+ return null;
+ }
+
+ interface Style extends CssResource {
+ String current();
+ String legacy_id();
+ String commit();
+ }
+
+ private final Change.Id changeId;
+ private final String revision;
+ private boolean loaded;
+ private JsArray<RevisionInfo> revisions;
+
+ @UiField FlexTable table;
+ @UiField Style style;
+
+ RevisionsBox(Change.Id changeId, String revision) {
+ this.changeId = changeId;
+ this.revision = revision;
+ initWidget(uiBinder.createAndBindUi(this));
+ }
+
+ @Override
+ protected void onLoad() {
+ if (!loaded) {
+ RestApi call = ChangeApi.detail(changeId.get());
+ ChangeList.addOptions(call, EnumSet.of(
+ ListChangesOption.ALL_REVISIONS,
+ ListChangesOption.ALL_COMMITS));
+ call.get(new AsyncCallback<ChangeInfo>() {
+ @Override
+ public void onSuccess(ChangeInfo result) {
+ render(result.revisions());
+ loaded = true;
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ }
+ });
+ }
+ }
+
+ private void onOpenRow(int idx) {
+ closeParent();
+ Gerrit.display(url(revisions.get(idx)));
+ }
+
+ private void render(NativeMap<RevisionInfo> map) {
+ map.copyKeysIntoChildren("name");
+
+ revisions = map.values();
+ RevisionInfo.sortRevisionInfoByNumber(revisions);
+ Collections.reverse(Natives.asList(revisions));
+
+ SafeHtmlBuilder sb = new SafeHtmlBuilder();
+ header(sb);
+ for (int i = 0; i < revisions.length(); i++) {
+ revision(sb, i, revisions.get(i));
+ }
+
+ GWT.<FancyFlexTableImpl> create(FancyFlexTableImpl.class)
+ .resetHtml(table, sb);
+ }
+
+ private void header(SafeHtmlBuilder sb) {
+ sb.openTr()
+ .openTh()
+ .setStyleName(style.legacy_id())
+ .append(Resources.C.ps())
+ .closeTh()
+ .openTh().append(Resources.C.commit()).closeTh()
+ .openTh().append(Resources.C.date()).closeTh()
+ .openTh().append(Resources.C.author()).closeTh()
+ .closeTr();
+ }
+
+ private void revision(SafeHtmlBuilder sb, int index, RevisionInfo r) {
+ CommitInfo c = r.commit();
+ sb.openTr();
+ if (revision.equals(r.name())) {
+ sb.setStyleName(style.current());
+ }
+
+ sb.openTd()
+ .setStyleName(style.legacy_id())
+ .append(r._number())
+ .closeTd();
+
+ sb.openTd()
+ .setStyleName(style.commit())
+ .openAnchor()
+ .setAttribute("href", "#" + url(r))
+ .setAttribute("onclick", OPEN + "(event," + index + ")")
+ .append(r.name().substring(0, 10))
+ .closeAnchor()
+ .closeTd();
+
+ sb.openTd()
+ .append(FormatUtil.shortFormatDayTime(c.committer().date()))
+ .closeTd();
+
+ String an = c.author() != null ? c.author().name() : null;
+ String cn = c.committer() != null ? c.committer().name() : null;
+ sb.openTd();
+ sb.append(an);
+ if (!"".equals(an) && !"".equals(cn) && !an.equals(cn)) {
+ sb.append(" / ").append(cn);
+ }
+ sb.closeTd();
+
+ sb.closeTr();
+ }
+
+ private String url(RevisionInfo r) {
+ return PageLinks.toChange2(
+ changeId,
+ String.valueOf(r._number()));
+ }
+
+ private void closeParent() {
+ for (Widget w = getParent(); w != null; w = w.getParent()) {
+ if (w instanceof PopupPanel) {
+ ((PopupPanel) w).hide(true);
+ break;
+ }
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.ui.xml
new file mode 100644
index 0000000..bf1ddd4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.ui.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+ <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+ <ui:style type='com.google.gerrit.client.change.RevisionsBox.Style'>
+ @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+
+ .revisionBox {
+ min-width: 300px;
+ margin: 10px 0px 5px 5px;
+ }
+
+ .scroll {
+ min-width: 300px;
+ height: 200px;
+ }
+
+ .table {
+ border-spacing: 0;
+ width: 100%;
+ }
+
+ .table td, .table th {
+ padding-left: 5px;
+ padding-right: 5px;
+ border-right: 2px solid #ddd;
+ white-space: nowrap;
+ }
+
+ .table tr.current {
+ background-color: selectionColor;
+ }
+
+ .legacy_id {
+ min-width: 50px;
+ text-align: right;
+ font-weight: bold;
+ }
+
+ .commit {
+ font-family: monospace;
+ }
+ </ui:style>
+ <g:HTMLPanel styleName='{style.revisionBox}'>
+ <g:ScrollPanel styleName='{style.scroll}'>
+ <g:FlexTable ui:field='table' styleName='{style.table}'/>
+ </g:ScrollPanel>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java
new file mode 100644
index 0000000..3424064
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RightSidePopdownAction.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+abstract class RightSidePopdownAction {
+ private final ChangeScreen2.Style style;
+ private final Widget button;
+ private final UIObject relativeTo;
+ private PopupPanel popup;
+
+ RightSidePopdownAction(
+ ChangeScreen2.Style style,
+ UIObject relativeTo,
+ Widget button) {
+ this.style = style;
+ this.relativeTo = relativeTo;
+ this.button = button;
+ }
+
+ abstract Widget getWidget();
+
+ void show() {
+ if (popup != null) {
+ button.removeStyleName(style.selected());
+ popup.hide();
+ return;
+ }
+
+ final PluginSafePopupPanel p = new PluginSafePopupPanel(true) {
+ @Override
+ public void setPopupPosition(int left, int top) {
+ top -= Document.get().getBodyOffsetTop();
+
+ int w = Window.getScrollLeft() + Window.getClientWidth();
+ int r = relativeTo.getAbsoluteLeft() + relativeTo.getOffsetWidth();
+ int right = w - r;
+ Style style = getElement().getStyle();
+ style.clearProperty("left");
+ style.setPropertyPx("right", right);
+ style.setPropertyPx("top", top);
+ }
+ };
+ p.setStyleName(style.replyBox());
+ p.addAutoHidePartner(button.getElement());
+ p.addCloseHandler(new CloseHandler<PopupPanel>() {
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ if (popup == p) {
+ button.removeStyleName(style.selected());
+ popup = null;
+ }
+ }
+ });
+ p.add(getWidget());
+ p.showRelativeTo(relativeTo);
+ GlobalKey.dialog(p);
+ button.addStyleName(style.selected());
+ popup = p;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/StarIcon.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/StarIcon.java
new file mode 100644
index 0000000..fccca27
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/StarIcon.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.ToggleButton;
+
+class StarIcon extends ToggleButton {
+ StarIcon() {
+ super(
+ new Image(Resources.I.star_open()),
+ new Image(Resources.I.star_filled()));
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
new file mode 100644
index 0000000..3008830
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.SubmitFailureDialog;
+import com.google.gerrit.client.changes.SubmitInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+
+class SubmitAction {
+ static void call(final Change.Id id, String revision) {
+ ChangeApi.submit(id.get(), revision,
+ new GerritCallback<SubmitInfo>() {
+ public void onSuccess(SubmitInfo result) {
+ redisplay();
+ }
+
+ public void onFailure(Throwable err) {
+ if (SubmitFailureDialog.isConflict(err)) {
+ new SubmitFailureDialog(err.getMessage()).center();
+ redisplay();
+ } else {
+ super.onFailure(err);
+ }
+ }
+
+ private void redisplay() {
+ Gerrit.display(PageLinks.toChange2(id));
+ }
+ });
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
new file mode 100644
index 0000000..fff109a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
@@ -0,0 +1,144 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+/** Displays (and edits) the change topic string. */
+class Topic extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, Topic> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ private PatchSet.Id psId;
+ private boolean canEdit;
+
+ @UiField FlowPanel show;
+ @UiField InlineLabel text;
+ @UiField Image editIcon;
+
+ @UiField Element form;
+ @UiField NpTextBox input;
+ @UiField NpTextArea message;
+ @UiField Button save;
+ @UiField Button cancel;
+
+ Topic() {
+ initWidget(uiBinder.createAndBindUi(this));
+ show.addDomHandler(
+ new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ onEdit();
+ }
+ },
+ ClickEvent.getType());
+ }
+
+ void set(ChangeInfo info, String revision) {
+ canEdit = info.has_actions()
+ && info.actions().containsKey("topic")
+ && info.actions().get("topic").enabled();
+
+ psId = new PatchSet.Id(
+ info.legacy_id(),
+ info.revisions().get(revision)._number());
+
+ text.setText(info.topic());
+ editIcon.setVisible(canEdit);
+ if (!canEdit) {
+ show.setTitle(null);
+ }
+ }
+
+ boolean canEdit() {
+ return canEdit;
+ }
+
+ void onEdit() {
+ if (canEdit) {
+ show.setVisible(false);
+ UIObject.setVisible(form, true);
+
+ input.setText(text.getText());
+ input.setFocus(true);
+ }
+ }
+
+ @UiHandler("cancel")
+ void onCancel(ClickEvent e) {
+ input.setFocus(false);
+ show.setVisible(true);
+ UIObject.setVisible(form, false);
+ }
+
+ @UiHandler("input")
+ void onKeyDownInput(KeyDownEvent e) {
+ if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
+ onCancel(null);
+ } else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
+ onSave(null);
+ }
+ }
+
+ @UiHandler("message")
+ void onKeyDownMessage(KeyDownEvent e) {
+ if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
+ onCancel(null);
+ } else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER
+ && e.isControlKeyDown()) {
+ onSave(null);
+ }
+ }
+
+ @UiHandler("save")
+ void onSave(ClickEvent e) {
+ ChangeApi.topic(
+ psId.getParentKey().get(),
+ input.getValue().trim(),
+ message.getValue().trim(),
+ new GerritCallback<String>() {
+ @Override
+ public void onSuccess(String result) {
+ Gerrit.display(PageLinks.toChange2(
+ psId.getParentKey(),
+ String.valueOf(psId.get())));
+ }
+ });
+ onCancel(null);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
new file mode 100644
index 0000000..2f4751c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+ <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
+ <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+ <ui:style>
+ .show { cursor: pointer; }
+ .edit, .cancel { float: right; }
+ </ui:style>
+ <g:HTMLPanel>
+ <g:FlowPanel ui:field='show'
+ styleName='{style.show}'
+ title='Click to edit topic (Shortcut: t)'>
+ <ui:attribute name='title'/>
+ <g:InlineLabel ui:field='text'/>
+ <g:Image ui:field='editIcon'
+ resource='{ico.edit}'
+ styleName='{style.edit}'/>
+ </g:FlowPanel>
+
+ <div ui:field='form' style='display: none' aria-hidden='true'>
+ <div>
+ <c:NpTextBox ui:field='input' visibleLength='55'/>
+ </div>
+ <div>
+ <c:NpTextArea ui:field='message'
+ visibleLines='3'
+ characterWidth='45'/>
+ </div>
+ <div>
+ <g:Button ui:field='save' styleName='{res.style.button}'>
+ <div>Update</div>
+ </g:Button>
+ <g:Button ui:field='cancel'
+ styleName='{res.style.button}'
+ addStyleNames='{style.cancel}'>
+ <div>Cancel</div>
+ </g:Button>
+ </div>
+ </div>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
new file mode 100644
index 0000000..2837b15
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
@@ -0,0 +1,151 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.Window.ScrollEvent;
+import com.google.gwt.user.client.Window.ScrollHandler;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.RootPanel;
+
+import java.sql.Timestamp;
+import java.util.HashSet;
+import java.util.List;
+
+/** Displays the "New Message From ..." panel in bottom right on updates. */
+abstract class UpdateAvailableBar extends PopupPanel {
+ interface Binder extends UiBinder<HTMLPanel, UpdateAvailableBar> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ static interface Style extends CssResource {
+ String popup();
+ }
+
+ private Timestamp updated;
+ private HandlerRegistration resizer;
+ private HandlerRegistration scroller;
+
+ @UiField Style style;
+ @UiField Element author;
+ @UiField Anchor show;
+ @UiField Anchor ignore;
+
+ UpdateAvailableBar() {
+ super(/* autoHide = */ false, /* modal = */ false);
+ add(uiBinder.createAndBindUi(this));
+ setStyleName(style.popup());
+ }
+
+ void set(List<MessageInfo> newMessages, Timestamp newTime) {
+ HashSet<Integer> seen = new HashSet<Integer>();
+ StringBuilder r = new StringBuilder();
+ for (MessageInfo m : newMessages) {
+ int a = m.author() != null ? m.author()._account_id() : 0;
+ if (seen.add(a)) {
+ if (r.length() > 0) {
+ r.append(", ");
+ }
+ r.append(Message.authorName(m));
+ }
+ }
+ author.setInnerText(r.toString());
+ updated = newTime;
+
+ if (isShowing()) {
+ setPopupPosition(
+ Window.getScrollLeft() + Window.getClientWidth() - getOffsetWidth(),
+ Window.getScrollTop() + Window.getClientHeight() - getOffsetHeight());
+ }
+ }
+
+ void popup() {
+ setPopupPositionAndShow(new PositionCallback() {
+ @Override
+ public void setPosition(int w, int h) {
+ w += 7; // Initial information is wrong, adjust with some slop.
+ h += 19;
+ setPopupPosition(
+ Window.getScrollLeft() + Window.getClientWidth() - w,
+ Window.getScrollTop() + Window.getClientHeight() - h);
+ }
+ });
+ if (resizer == null) {
+ resizer = Window.addResizeHandler(new ResizeHandler() {
+ @Override
+ public void onResize(ResizeEvent event) {
+ setPopupPosition(
+ Window.getScrollLeft() + event.getWidth() - getOffsetWidth(),
+ Window.getScrollTop() + event.getHeight() - getOffsetHeight());
+ }
+ });
+ }
+ if (scroller == null) {
+ scroller = Window.addWindowScrollHandler(new ScrollHandler() {
+ @Override
+ public void onWindowScroll(ScrollEvent event) {
+ RootPanel b = Gerrit.getBottomMenu();
+ int br = b.getAbsoluteLeft() + b.getOffsetWidth();
+ int bp = b.getAbsoluteTop() + b.getOffsetHeight();
+ int wr = event.getScrollLeft() + Window.getClientWidth();
+ int wp = event.getScrollTop() + Window.getClientHeight();
+ setPopupPosition(
+ Math.min(br, wr) - getOffsetWidth(),
+ Math.min(bp, wp) - getOffsetHeight());
+ }
+ });
+ }
+ }
+
+ @Override
+ public void hide() {
+ if (resizer != null) {
+ resizer.removeHandler();
+ resizer = null;
+ }
+ if (scroller != null) {
+ scroller.removeHandler();
+ scroller = null;
+ }
+ super.hide();
+ }
+
+ @UiHandler("show")
+ void onShow(ClickEvent e) {
+ onShow();
+ }
+
+ @UiHandler("ignore")
+ void onIgnore(ClickEvent e) {
+ onIgnore(updated);
+ hide();
+ }
+
+ abstract void onShow();
+ abstract void onIgnore(Timestamp newTime);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml
new file mode 100644
index 0000000..a6cd124
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+ <ui:style type='com.google.gerrit.client.change.UpdateAvailableBar.Style'>
+ .popup {
+ padding: 5px;
+ }
+ .bar {
+ background-color: #fff1a8;
+ border: 1px solid #ccc;
+ padding: 5px 10px;
+ font-size: 80%;
+ color: #222;
+ white-space: nowrap;
+ width: auto;
+ height: auto;
+ }
+ a.action {
+ color: #222;
+ text-decoration: underline;
+ display: inline-block;
+ margin-left: 0.5em;
+ }
+ </ui:style>
+ <g:HTMLPanel styleName='{style.bar}'>
+ <ui:msg>Update from <span ui:field='author'/></ui:msg>
+ <g:Anchor ui:field='show'
+ styleName='{style.action}'
+ href='javascript:;'
+ title='Refresh screen and display updates'>
+ <ui:attribute name='title'/>
+ <ui:msg>Show</ui:msg>
+ </g:Anchor>
+ <g:Anchor ui:field='ignore'
+ styleName='{style.action}'
+ href='javascript:;'
+ title='Ignore this update'>
+ <ui:attribute name='title'/>
+ <ui:msg>Ignore</ui:msg>
+ </g:Anchor>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java
new file mode 100644
index 0000000..9d2a467
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateCheckTimer.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.ui.UserActivityMonitor;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+class UpdateCheckTimer extends Timer implements ValueChangeHandler<Boolean> {
+ private static final int MAX_PERIOD = 3 * 60 * 1000;
+ private static final int IDLE_PERIOD = 2 * 3600 * 1000;
+ private static final int POLL_PERIOD =
+ Gerrit.getConfig().getChangeUpdateDelay() * 1000;
+
+ private final ChangeScreen2 screen;
+ private int delay;
+ private boolean running;
+
+ UpdateCheckTimer(ChangeScreen2 screen) {
+ this.screen = screen;
+ this.delay = POLL_PERIOD;
+ }
+
+ void schedule() {
+ scheduleRepeating(delay);
+ }
+
+ @Override
+ public void run() {
+ if (!screen.isAttached()) {
+ // screen should have cancelled this timer.
+ cancel();
+ return;
+ } else if (running) {
+ return;
+ }
+
+ running = true;
+ screen.loadChangeInfo(false, new AsyncCallback<ChangeInfo>() {
+ @Override
+ public void onSuccess(ChangeInfo info) {
+ running = false;
+ screen.showUpdates(info);
+
+ int d = UserActivityMonitor.isActive()
+ ? POLL_PERIOD
+ : IDLE_PERIOD;
+ if (d != delay) {
+ delay = d;
+ schedule();
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ // On failures increase the delay time and try again,
+ // but place an upper bound on the delay.
+ running = false;
+ delay = (int) Math.max(
+ delay * (1.5 + Math.random()),
+ UserActivityMonitor.isActive()
+ ? MAX_PERIOD
+ : IDLE_PERIOD + MAX_PERIOD);
+ schedule();
+ }
+ });
+ }
+
+ @Override
+ public void onValueChange(ValueChangeEvent<Boolean> event) {
+ if (event.getValue()) {
+ delay = POLL_PERIOD;
+ run();
+ } else {
+ delay = IDLE_PERIOD;
+ }
+ schedule();
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css
new file mode 100644
index 0000000..3dde21a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/common.css
@@ -0,0 +1,56 @@
+/* Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+
+.popup {
+ background-color: trimColor;
+ min-width: 300px;
+ min-height: 90px;
+}
+
+.popupContent {
+ padding: 5px;
+}
+
+.button,
+.popup button,
+.popup input[type='button'] {
+ margin: 0 3px 0 0;
+ border-color: rgba(0, 0, 0, 0.1);
+ text-align: center;
+ font-size: 11px;
+ font-weight: bold;
+ border: 1px solid;
+ cursor: pointer;
+ color: #fff;
+ background-color: #4d90fe;
+ background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
+ -webkit-border-radius: 2px;
+ -webkit-box-sizing: content-box;
+}
+
+.button div, .popup button div {
+ width: 54px;
+ white-space: nowrap;
+ color: #fff;
+ height: 10px;
+ line-height: 10px;
+}
+
+.section {
+ padding: 5px 5px;
+ border-bottom: 1px solid #b8b8b8;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
new file mode 100644
index 0000000..943f9f1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
@@ -0,0 +1,67 @@
+/* Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.pointer, .reviewed {
+ width: 12px;
+ padding: 0px;
+ vertical-align: top;
+}
+
+.pathColumn {
+ white-space: nowrap;
+ min-width: 600px;
+}
+.pathColumn a {
+ color: #000;
+}
+.commonPrefix {
+ color: #888;
+}
+
+.draftColumn,
+.newColumn,
+.commentColumn {
+ white-space: nowrap;
+}
+.draftColumn {
+ color: #d44;
+ font-weight: bold;
+}
+.newColumn {
+ font-weight: bold;
+}
+
+.deltaColumn1 {
+ white-space: nowrap;
+ text-align: right;
+}
+
+.deltaColumn2 {
+ padding-left: 5px;
+ white-space: nowrap;
+ text-align: right;
+}
+
+.inserted {
+ height: 10px;
+ display: inline-block;
+ background-color: #4d4;
+}
+
+.deleted {
+ height: 10px;
+ display: inline-block;
+ background-color: #d44;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/reload_black.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/reload_black.png
new file mode 100644
index 0000000..13f3a5d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/reload_black.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/reload_white.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/reload_white.png
new file mode 100644
index 0000000..8f5a311
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/reload_white.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_filled.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_filled.png
new file mode 100644
index 0000000..39bddb1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_filled.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_open.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_open.png
new file mode 100644
index 0000000..6c955de
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/star_open.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index 2580542..5f89bf0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -19,11 +19,15 @@
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.common.changes.ListChangesOption;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gwt.core.client.JsArray;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
import java.util.Collections;
import java.util.Comparator;
+import java.util.EnumSet;
public class AccountDashboardScreen extends Screen implements ChangeListScreen {
private final Account.Id ownerId;
@@ -41,7 +45,16 @@
@Override
protected void onInitUI() {
super.onInitUI();
- table = new ChangeTable2();
+ table = new ChangeTable2() {
+ {
+ keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadSearch()) {
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ Gerrit.display(getToken());
+ }
+ });
+ }
+ };
table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
outgoing = new ChangeTable2.Section();
@@ -50,7 +63,7 @@
outgoing.setTitleText(Util.C.outgoingReviews());
incoming.setTitleText(Util.C.incomingReviews());
- incoming.setHighlightUnreviewed(true);
+ incoming.setHighlightUnreviewed(mine);
closed.setTitleText(Util.C.recentlyClosed());
table.addSection(outgoing);
@@ -63,6 +76,7 @@
@Override
protected void onLoad() {
super.onLoad();
+
String who = mine ? "self" : ownerId.toString();
ChangeList.query(
new ScreenLoadCallback<JsArray<ChangeList>>(this) {
@@ -71,6 +85,9 @@
display(result);
}
},
+ mine
+ ? EnumSet.of(ListChangesOption.REVIEWED)
+ : EnumSet.noneOf(ListChangesOption.class),
"is:open owner:" + who,
"is:open reviewer:" + who + " -owner:" + who,
"is:closed owner:" + who + " -age:4w limit:10");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
index 08877d9..93c9505 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -52,6 +52,7 @@
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -105,6 +106,13 @@
setStyleName(Gerrit.RESOURCES.css().approvalTable());
}
+ /**
+ * Sets the header row
+ *
+ * @param labels The list of labels to display in the header. This list does
+ * not get resorted, so be sure that the list's elements are in the same
+ * order as the list of labels passed to the {@code displayRow} method.
+ */
private void displayHeader(Collection<String> labels) {
table.resizeColumns(2 + labels.size());
@@ -144,12 +152,14 @@
if (byUser.isEmpty()) {
table.setVisible(false);
} else {
- displayHeader(change.labels());
+ List<String> labels = new ArrayList<String>(change.labels());
+ Collections.sort(labels);
+ displayHeader(labels);
table.resizeRows(1 + byUser.size());
int i = 1;
for (ApprovalDetail ad : ApprovalDetail.sort(
byUser.values(), change.owner()._account_id())) {
- displayRow(i++, ad, change, accounts.get(ad.getAccount().get()));
+ displayRow(i++, ad, labels, accounts.get(ad.getAccount().get()));
}
table.setVisible(true);
}
@@ -324,8 +334,18 @@
});
}
- private void displayRow(int row, final ApprovalDetail ad, ChangeInfo change,
- AccountInfo account) {
+ /**
+ * Sets the reviewer data for a row.
+ *
+ * @param row The number of the row on which to set the reviewer.
+ * @param ad The details for this reviewer's approval.
+ * @param labels The list of labels to show. This list does not get resorted,
+ * so be sure that the list's elements are in the same order as the list
+ * of labels passed to the {@code displayHeader} method.
+ * @param account The account information for the approval.
+ */
+ private void displayRow(int row, final ApprovalDetail ad,
+ List<String> labels, AccountInfo account) {
final CellFormatter fmt = table.getCellFormatter();
int col = 0;
@@ -351,7 +371,7 @@
}
fmt.setStyleName(row, col++, Gerrit.RESOURCES.css().removeReviewerCell());
- for (String labelName : change.labels()) {
+ for (String labelName : labels) {
fmt.setStyleName(row, col, Gerrit.RESOURCES.css().approvalscore());
if (!ad.canVote(labelName)) {
fmt.addStyleName(row, col, Gerrit.RESOURCES.css().notVotable());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index abd94c9..c292154 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -62,11 +62,24 @@
}
public static void detail(int id, AsyncCallback<ChangeInfo> cb) {
- call(id, "detail").get(cb);
+ detail(id).get(cb);
+ }
+
+ public static RestApi detail(int id) {
+ return call(id, "detail");
+ }
+
+ public static RestApi revision(int id, String revision) {
+ return change(id).view("revisions").id(revision);
}
public static RestApi revision(PatchSet.Id id) {
- return change(id.getParentKey().get()).view("revisions").id(id.get());
+ int cn = id.getParentKey().get();
+ String revision = RevisionInfoCache.get(id);
+ if (revision != null) {
+ return revision(cn, revision);
+ }
+ return change(cn).view("revisions").id(id.get());
}
public static RestApi reviewers(int id) {
@@ -82,12 +95,34 @@
}
/** Submit a specific revision of a change. */
+ public static void cherrypick(int id, String commit, String destination, String message, AsyncCallback<ChangeInfo> cb) {
+ CherryPickInput cherryPickInput = CherryPickInput.create();
+ cherryPickInput.setMessage(message);
+ cherryPickInput.setDestination(destination);
+ call(id, commit, "cherrypick").post(cherryPickInput, cb);
+ }
+
+ /** Edit commit message for specific revision of a change. */
+ public static void message(int id, String commit, String message,
+ AsyncCallback<JavaScriptObject> cb) {
+ CherryPickInput input = CherryPickInput.create();
+ input.setMessage(message);
+ call(id, commit, "message").post(input, cb);
+ }
+
+ /** Submit a specific revision of a change. */
public static void submit(int id, String commit, AsyncCallback<SubmitInfo> cb) {
SubmitInput in = SubmitInput.create();
in.wait_for_merge(true);
call(id, commit, "submit").post(in, cb);
}
+ /** Rebase a revision onto the branch tip. */
+ public static void rebase(int id, String commit, AsyncCallback<ChangeInfo> cb) {
+ JavaScriptObject in = JavaScriptObject.createObject();
+ call(id, commit, "rebase").post(in, cb);
+ }
+
private static class Input extends JavaScriptObject {
final native void topic(String t) /*-{ if(t)this.topic=t; }-*/;
final native void message(String m) /*-{ if(m)this.message=m; }-*/;
@@ -100,6 +135,17 @@
}
}
+ private static class CherryPickInput extends JavaScriptObject {
+ static CherryPickInput create() {
+ return (CherryPickInput) createObject();
+ }
+ final native void setDestination(String d) /*-{ this.destination = d; }-*/;
+ final native void setMessage(String m) /*-{ this.message = m; }-*/;
+
+ protected CherryPickInput() {
+ }
+ };
+
private static class SubmitInput extends JavaScriptObject {
final native void wait_for_merge(boolean b) /*-{ this.wait_for_merge=b; }-*/;
@@ -119,7 +165,7 @@
return change(id).view("revisions").id(commit).view(action);
}
- private static RestApi change(int id) {
+ public static RestApi change(int id) {
// TODO Switch to triplet project~branch~id format in URI.
return new RestApi("/changes/").id(String.valueOf(id));
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 968c726..197b466 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -22,6 +22,9 @@
String statusLongMerged();
String statusLongAbandoned();
String statusLongDraft();
+ String readyToSubmit();
+ String mergeConflict();
+ String notCurrent();
String myDashboardTitle();
String unknownDashboardTitle();
@@ -37,6 +40,7 @@
String allMergedChanges();
String changeTableColumnSubject();
+ String changeTableColumnStatus();
String changeTableColumnOwner();
String changeTableColumnReviewers();
String changeTableColumnProject();
@@ -52,7 +56,11 @@
String expandCollapseDependencies();
String previousPatchSet();
String nextPatchSet();
+ String keyReloadChange();
+ String keyReloadSearch();
String keyPublishComments();
+ String keyEditTopic();
+ String keyEditMessage();
String patchTableColumnName();
String patchTableColumnComments();
@@ -62,6 +70,7 @@
String patchTableDiffUnified();
String patchTableDownloadPreImage();
String patchTableDownloadPostImage();
+ String patchTableBinary();
String commitMessage();
String fileCommentHeader();
@@ -119,6 +128,9 @@
String messageCollapseAll();
String messageNeedsRebaseOrHasDependency();
+ String sideBySide();
+ String unifiedDiff();
+
String patchSetInfoAuthor();
String patchSetInfoCommitter();
String patchSetInfoDownload();
@@ -137,6 +149,12 @@
String editCommitMessageToolTip();
String titleEditCommitMessage();
+ String buttonCherryPickChangeBegin();
+ String buttonCherryPickChangeSend();
+ String headingCherryPickBranch();
+ String cherryPickCommitMessage();
+ String cherryPickTitle();
+
String buttonAbandonChangeBegin();
String buttonAbandonChangeSend();
String headingAbandonMessage();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 4c12378..f51cff0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -3,6 +3,9 @@
statusLongMerged = Merged
statusLongAbandoned = Abandoned
statusLongDraft = Draft
+readyToSubmit = Ready to Submit
+mergeConflict = Merge Conflict
+notCurrent = Not Current
starredHeading = Starred Changes
watchedHeading = Open Changes of Watched Projects
@@ -17,6 +20,7 @@
allMergedChanges = All merged changes
changeTableColumnSubject = Subject
+changeTableColumnStatus = Status
changeTableColumnOwner = Owner
changeTableColumnReviewers = Reviewers
changeTableColumnProject = Project
@@ -32,7 +36,12 @@
expandCollapseDependencies = Expands / Collapses dependencies section
previousPatchSet = Previous patch set
nextPatchSet = Next patch set
+keyReloadChange = Reload change
+keyReloadSearch = Reload change list
keyPublishComments = Review and publish comments
+keyEditTopic = Edit change topic
+keyEditMessage = Edit commit message
+
patchTableColumnName = File Path
patchTableColumnComments = Comments
@@ -42,6 +51,7 @@
patchTableDiffUnified = Unified
patchTableDownloadPreImage = old
patchTableDownloadPostImage = new
+patchTableBinary = Binary
commitMessage = Commit Message
fileCommentHeader = File Comment:
@@ -96,6 +106,9 @@
messageCollapseAll = Collapse All
messageNeedsRebaseOrHasDependency = Need Rebase or Has Dependency
+sideBySide = Side by Side
+unifiedDiff = Unified Diff
+
patchSetInfoAuthor = Author
patchSetInfoCommitter = Committer
patchSetInfoDownload = Download
@@ -122,6 +135,12 @@
editCommitMessageToolTip = Edit Commit Message
titleEditCommitMessage = Create New Patch Set
+buttonCherryPickChangeBegin = Cherry Pick To
+buttonCherryPickChangeSend = Cherry Pick Change
+headingCherryPickBranch = Cherry Pick to Branch:
+cherryPickCommitMessage = Cherry Pick Commit Message:
+cherryPickTitle = Code Review - Cherry Pick Change to Another Branch
+
buttonRestoreChangeBegin = Restore Change
restoreChangeTitle = Code Review - Restore Change
headingRestoreMessage = Restore Message:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
index 2fcb7e8..6b13ba0a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
@@ -37,12 +37,12 @@
initWidget(hp);
}
- public void display(ChangeDetail chg, Boolean starred, Boolean canEditCommitMessage,
+ public void display(ChangeDetail changeDetail, Boolean starred, Boolean canEditCommitMessage,
PatchSetInfo info, AccountInfoCache acc,
SubmitTypeRecord submitTypeRecord,
CommentLinkProcessor commentLinkProcessor) {
- infoBlock.display(chg, acc, submitTypeRecord);
- messageBlock.display(chg.getChange().currentPatchSetId(), starred,
+ infoBlock.display(changeDetail, acc, submitTypeRecord);
+ messageBlock.display(changeDetail.getChange().currentPatchSetId(), starred,
canEditCommitMessage, info.getMessage(), commentLinkProcessor);
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
index 00693d9..a6448dc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -15,9 +15,12 @@
package com.google.gerrit.client.changes;
import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.actions.ActionInfo;
+import com.google.gerrit.client.diff.FileInfo;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.common.data.LabelValue;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
@@ -27,12 +30,16 @@
import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
public class ChangeInfo extends JavaScriptObject {
public final void init() {
- if (labels0() != null) {
- labels0().copyKeysIntoChildren("_name");
+ if (all_labels() != null) {
+ all_labels().copyKeysIntoChildren("_name");
}
}
@@ -69,7 +76,7 @@
}
public final Set<String> labels() {
- return labels0().keySet();
+ return all_labels().keySet();
}
public final native String id() /*-{ return this.id; }-*/;
@@ -86,22 +93,26 @@
public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
public final native String _sortkey() /*-{ return this._sortkey; }-*/;
- private final native NativeMap<LabelInfo> labels0() /*-{ return this.labels; }-*/;
+ public final native NativeMap<LabelInfo> all_labels() /*-{ return this.labels; }-*/;
public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
+ public final native String current_revision() /*-{ return this.current_revision; }-*/;
+ public final native NativeMap<RevisionInfo> revisions() /*-{ return this.revisions; }-*/;
+ public final native RevisionInfo revision(String n) /*-{ return this.revisions[n]; }-*/;
+ public final native JsArray<MessageInfo> messages() /*-{ return this.messages; }-*/;
public final native boolean has_permitted_labels()
/*-{ return this.hasOwnProperty('permitted_labels') }-*/;
- private final native NativeMap<JavaScriptObject> _permitted_labels()
+ public final native NativeMap<JsArrayString> permitted_labels()
/*-{ return this.permitted_labels; }-*/;
- public final Set<String> permitted_labels() {
- return Natives.keys(_permitted_labels());
- }
public final native JsArrayString permitted_values(String n)
/*-{ return this.permitted_labels[n]; }-*/;
public final native JsArray<AccountInfo> removable_reviewers()
/*-{ return this.removable_reviewers; }-*/;
+ public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
+ public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
+
final native int _number() /*-{ return this._number; }-*/;
final native boolean _more_changes()
/*-{ return this._more_changes ? true : false; }-*/;
@@ -130,9 +141,17 @@
public final native AccountInfo disliked() /*-{ return this.disliked; }-*/;
public final native JsArray<ApprovalInfo> all() /*-{ return this.all; }-*/;
+ public final ApprovalInfo for_user(int user) {
+ JsArray<ApprovalInfo> all = all();
+ for (int i = 0; all != null && i < all.length(); i++) {
+ if (all.get(i)._account_id() == user) {
+ return all.get(i);
+ }
+ }
+ return null;
+ }
private final native NativeMap<NativeString> _values() /*-{ return this.values; }-*/;
-
public final Set<String> values() {
return Natives.keys(_values());
}
@@ -147,15 +166,119 @@
return 0;
}-*/;
+ public final String max_value() {
+ return LabelValue.formatValue(value_set().last());
+ }
+
+ public final SortedSet<Short> value_set() {
+ SortedSet<Short> values = new TreeSet<Short>();
+ for (String v : values()) {
+ values.add(parseValue(v));
+ }
+ return values;
+ }
+
+ public static final short parseValue(String formatted) {
+ if (formatted.startsWith("+")) {
+ formatted = formatted.substring(1);
+ } else if (formatted.startsWith(" ")) {
+ formatted = formatted.trim();
+ }
+ return Short.parseShort(formatted);
+ }
+
protected LabelInfo() {
}
}
public static class ApprovalInfo extends AccountInfo {
public final native boolean has_value() /*-{ return this.hasOwnProperty('value'); }-*/;
- public final native short value() /*-{ return this.value; }-*/;
+ public final native short value() /*-{ return this.value || 0; }-*/;
protected ApprovalInfo() {
}
}
+
+ public static class RevisionInfo extends JavaScriptObject {
+ public final native int _number() /*-{ return this._number; }-*/;
+ public final native String name() /*-{ return this.name; }-*/;
+ public final native boolean draft() /*-{ return this.draft || false; }-*/;
+ public final native CommitInfo commit() /*-{ return this.commit; }-*/;
+ public final native void set_commit(CommitInfo c) /*-{ this.commit = c; }-*/;
+
+ public final native boolean has_files() /*-{ return this.hasOwnProperty('files') }-*/;
+ public final native NativeMap<FileInfo> files() /*-{ return this.files; }-*/;
+
+ public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
+ public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
+
+ public final native boolean has_fetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
+ public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
+
+ public static void sortRevisionInfoByNumber(JsArray<RevisionInfo> list) {
+ Collections.sort(Natives.asList(list), new Comparator<RevisionInfo>() {
+ @Override
+ public int compare(RevisionInfo a, RevisionInfo b) {
+ return a._number() - b._number();
+ }
+ });
+ }
+
+ protected RevisionInfo () {
+ }
+ }
+
+ public static class FetchInfo extends JavaScriptObject {
+ public final native String url() /*-{ return this.url }-*/;
+ public final native String ref() /*-{ return this.ref }-*/;
+
+ protected FetchInfo () {
+ }
+ }
+
+ public static class CommitInfo extends JavaScriptObject {
+ public final native String commit() /*-{ return this.commit; }-*/;
+ public final native JsArray<CommitInfo> parents() /*-{ return this.parents; }-*/;
+ public final native GitPerson author() /*-{ return this.author; }-*/;
+ public final native GitPerson committer() /*-{ return this.committer; }-*/;
+ public final native String subject() /*-{ return this.subject; }-*/;
+ public final native String message() /*-{ return this.message; }-*/;
+
+ protected CommitInfo() {
+ }
+ }
+
+ public static class GitPerson extends JavaScriptObject {
+ public final native String name() /*-{ return this.name; }-*/;
+ public final native String email() /*-{ return this.email; }-*/;
+ private final native String dateRaw() /*-{ return this.date; }-*/;
+
+ public final Timestamp date() {
+ return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
+ }
+
+ protected GitPerson() {
+ }
+ }
+
+ public static class MessageInfo extends JavaScriptObject {
+ public final native AccountInfo author() /*-{ return this.author; }-*/;
+ public final native String message() /*-{ return this.message; }-*/;
+ private final native String dateRaw() /*-{ return this.date; }-*/;
+
+ public final Timestamp date() {
+ return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
+ }
+
+ protected MessageInfo() {
+ }
+ }
+
+ public static class MergeableInfo extends JavaScriptObject {
+ public final native String submit_type() /*-{ return this.submit_type }-*/;
+ public final native boolean mergeable() /*-{ return this.mergeable }-*/;
+
+ protected MergeableInfo() {
+ }
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
index 7c41ea1..773313a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
@@ -28,13 +28,18 @@
/** Run 2 or more queries in a single remote invocation. */
public static void query(
- AsyncCallback<JsArray<ChangeList>> callback, String... queries) {
+ AsyncCallback<JsArray<ChangeList>> callback,
+ EnumSet<ListChangesOption> options,
+ String... queries) {
assert queries.length >= 2; // At least 2 is required for correct result.
RestApi call = new RestApi(URI);
for (String q : queries) {
call.addParameterRaw("q", KeyUtil.encode(q));
}
- addOptions(call, ListChangesOption.LABELS);
+
+ EnumSet<ListChangesOption> o = EnumSet.of(ListChangesOption.LABELS);
+ o.addAll(options);
+ addOptions(call, o);
call.get(callback);
}
@@ -45,7 +50,7 @@
if (limit > 0) {
call.addParameter("n", limit);
}
- addOptions(call, ListChangesOption.LABELS);
+ addOptions(call, EnumSet.of(ListChangesOption.LABELS));
if (!PagedSingleListScreen.MIN_SORTKEY.equals(sortkey)) {
call.addParameter("P", sortkey);
}
@@ -59,16 +64,14 @@
if (limit > 0) {
call.addParameter("n", limit);
}
- addOptions(call, ListChangesOption.LABELS);
+ addOptions(call, EnumSet.of(ListChangesOption.LABELS));
if (!PagedSingleListScreen.MAX_SORTKEY.equals(sortkey)) {
call.addParameter("N", sortkey);
}
call.get(callback);
}
- private static void addOptions(
- RestApi call, ListChangesOption option1, ListChangesOption... options) {
- EnumSet<ListChangesOption> s = EnumSet.of(option1, options);
+ public static void addOptions(RestApi call, EnumSet<ListChangesOption> s) {
call.addParameterRaw("O", Integer.toHexString(ListChangesOption.toBits(s)));
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index 098fe07..4d1ec7e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -24,6 +24,7 @@
String revertChangeDefaultMessage(String commitMsg, String commitId);
+ String cherryPickedChangeDefaultMessage(String commitMsg, String commitId);
String changeScreenTitleId(String changeId);
String outdatedHeader(int outdated);
String patchSetHeader(int id);
@@ -33,6 +34,7 @@
String patchTableComments(@PluralCount int count);
String patchTableDrafts(@PluralCount int count);
String patchTableSize_Modify(int insertions, int deletions);
+ String patchTableSize_LongModify(int insertions, int deletions);
String patchTableSize_Lines(@PluralCount int insertions);
String removeReviewer(String fullName);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index e02c27c..f7dee85 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -6,6 +6,7 @@
changesAbandonedInProject = Abandoned Changes In {0}
revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}.
+cherryPickedChangeDefaultMessage = {0}\n(cherry picked from commit {1})
changeScreenTitleId = Change {0}
outdatedHeader = Change depends on {0} outdated change(s) and should be rebased on the latest patch sets.
@@ -16,6 +17,7 @@
patchTableComments = {0} comments
patchTableDrafts = {0} drafts
patchTableSize_Modify = +{0}, -{1}
+patchTableSize_LongModify = {0} inserted, {1} deleted
patchTableSize_Lines = {0} lines
removeReviewer = Remove reviewer {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index c69f915..f2b74d6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
@@ -288,7 +288,7 @@
// Handled by last callback's onFailure.
}
}));
- ChangeApi.detail(event.getValue().getChange().getId().get(), cbs.add(
+ ChangeApi.detail(event.getValue().getChange().getId().get(), cbs.addFinal(
new GerritCallback<com.google.gerrit.client.changes.ChangeInfo>() {
@Override
public void onSuccess(
@@ -339,13 +339,11 @@
patchesList.addItem(Util.C.baseDiffItem());
}
for (PatchSet pId : detail.getPatchSets()) {
- if (patchesList != null) {
- patchesList.addItem(Util.M.patchSetHeader(pId.getPatchSetId()), pId
- .getId().toString());
- }
+ patchesList.addItem(Util.M.patchSetHeader(pId.getPatchSetId()), pId
+ .getId().toString());
}
- if (diffBaseId != null && patchesList != null) {
+ if (diffBaseId != null) {
patchesList.setSelectedIndex(diffBaseId.get());
}
@@ -422,7 +420,7 @@
final Timestamp aged = new Timestamp(System.currentTimeMillis() - AGE);
CommentVisibilityStrategy commentVisibilityStrategy =
- CommentVisibilityStrategy.EXPAND_MOST_RECENT;
+ CommentVisibilityStrategy.EXPAND_RECENT;
if (Gerrit.isSignedIn()) {
commentVisibilityStrategy = Gerrit.getUserAccount()
.getGeneralPreferences().getCommentVisibilityStrategy();
@@ -457,16 +455,16 @@
switch (commentVisibilityStrategy) {
case COLLAPSE_ALL:
break;
- case EXPAND_RECENT:
- isOpen = isRecent;
- break;
case EXPAND_ALL:
isOpen = true;
break;
case EXPAND_MOST_RECENT:
- default:
isOpen = i == msgList.size() - 1;
break;
+ case EXPAND_RECENT:
+ default:
+ isOpen = isRecent;
+ break;
}
cp.setOpen(isOpen);
comments.add(cp);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
index 4694272..f993ad9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
@@ -48,11 +48,12 @@
public class ChangeTable2 extends NavigationTable<ChangeInfo> {
private static final int C_STAR = 1;
private static final int C_SUBJECT = 2;
- private static final int C_OWNER = 3;
- private static final int C_PROJECT = 4;
- private static final int C_BRANCH = 5;
- private static final int C_LAST_UPDATE = 6;
- private static final int BASE_COLUMNS = 7;
+ private static final int C_STATUS = 3;
+ private static final int C_OWNER = 4;
+ private static final int C_PROJECT = 5;
+ private static final int C_BRANCH = 6;
+ private static final int C_LAST_UPDATE = 7;
+ private static final int BASE_COLUMNS = 8;
private final List<Section> sections;
private int columns;
@@ -70,6 +71,7 @@
sections = new ArrayList<Section>();
table.setText(0, C_STAR, "");
table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
+ table.setText(0, C_STATUS, Util.C.changeTableColumnStatus());
table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
table.setText(0, C_PROJECT, Util.C.changeTableColumnProject());
table.setText(0, C_BRANCH, Util.C.changeTableColumnBranch());
@@ -90,6 +92,8 @@
}
if (cell.getCellIndex() == C_STAR) {
// Don't do anything (handled by star itself).
+ } else if (cell.getCellIndex() == C_STATUS) {
+ // Don't do anything.
} else if (cell.getCellIndex() == C_OWNER) {
// Don't do anything.
} else if (getRowItem(cell.getRowIndex()) != null) {
@@ -108,7 +112,7 @@
protected void onOpenRow(final int row) {
final ChangeInfo c = getRowItem(row);
final Change.Id id = c.legacy_id();
- Gerrit.display(PageLinks.toChange(id), new ChangeScreen(id));
+ Gerrit.display(PageLinks.toChange(id));
}
private void insertNoneRow(final int row) {
@@ -191,11 +195,12 @@
}
String subject = Util.cropSubject(c.subject());
+ table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
+
Change.Status status = c.status();
if (status != Change.Status.NEW) {
- subject += " (" + Util.toLongString(status) + ")";
+ table.setText(row, C_STATUS, Util.toLongString(status));
}
- table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
if (c.owner() != null) {
table.setWidget(row, C_OWNER, new AccountLinkPanel(c.owner(), status));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java
new file mode 100644
index 0000000..d87cb5e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentApi.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+public class CommentApi {
+
+ public static void comments(PatchSet.Id id,
+ AsyncCallback<NativeMap<JsArray<CommentInfo>>> cb) {
+ revision(id, "comments").get(cb);
+ }
+
+ public static void comment(PatchSet.Id id, String commentId,
+ AsyncCallback<CommentInfo> cb) {
+ revision(id, "comments").id(commentId).get(cb);
+ }
+
+ public static void drafts(PatchSet.Id id,
+ AsyncCallback<NativeMap<JsArray<CommentInfo>>> cb) {
+ revision(id, "drafts").get(cb);
+ }
+
+ public static void draft(PatchSet.Id id, String draftId,
+ AsyncCallback<CommentInfo> cb) {
+ revision(id, "drafts").id(draftId).get(cb);
+ }
+
+ public static void createDraft(PatchSet.Id id, CommentInput content,
+ AsyncCallback<CommentInfo> cb) {
+ revision(id, "drafts").put(content, cb);
+ }
+
+ public static void updateDraft(PatchSet.Id id, String draftId,
+ CommentInput content, AsyncCallback<CommentInfo> cb) {
+ revision(id, "drafts").id(draftId).put(content, cb);
+ }
+
+ public static void deleteDraft(PatchSet.Id id, String draftId,
+ AsyncCallback<JavaScriptObject> cb) {
+ revision(id, "drafts").id(draftId).delete(cb);
+ }
+
+ private static RestApi revision(PatchSet.Id id, String type) {
+ return ChangeApi.revision(id).view(type);
+ }
+
+ private CommentApi() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
new file mode 100644
index 0000000..34c0638
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.diff.CommentRange;
+import com.google.gerrit.common.changes.Side;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
+
+import java.sql.Timestamp;
+
+public class CommentInfo extends JavaScriptObject {
+ public static CommentInfo createRange(String path, Side side, int line,
+ String in_reply_to, String message, CommentRange range) {
+ CommentInfo info = createFile(path, side, in_reply_to, message);
+ info.setRange(range);
+ info.setLine(range == null ? line : range.end_line());
+ return info;
+ }
+
+ public static CommentInfo createFile(String path, Side side,
+ String in_reply_to, String message) {
+ CommentInfo info = createObject().cast();
+ info.setPath(path);
+ info.setSide(side);
+ info.setInReplyTo(in_reply_to);
+ info.setMessage(message);
+ return info;
+ }
+
+ private final native void setId(String id) /*-{ this.id = id; }-*/;
+ private final native void setPath(String path) /*-{ this.path = path; }-*/;
+
+ private final void setSide(Side side) {
+ setSideRaw(side.toString());
+ }
+ private final native void setSideRaw(String side) /*-{ this.side = side; }-*/;
+
+ private final native void setLine(int line) /*-{ this.line = line; }-*/;
+
+ private final native void setInReplyTo(String in_reply_to) /*-{
+ this.in_reply_to = in_reply_to;
+ }-*/;
+
+ private final native void setMessage(String message) /*-{ this.message = message; }-*/;
+
+ public final native String id() /*-{ return this.id; }-*/;
+ public final native String path() /*-{ return this.path; }-*/;
+
+ public final Side side() {
+ String s = sideRaw();
+ return s != null
+ ? Side.valueOf(s)
+ : Side.REVISION;
+ }
+ private final native String sideRaw() /*-{ return this.side }-*/;
+
+ public final native int line() /*-{ return this.line; }-*/;
+ public final native String in_reply_to() /*-{ return this.in_reply_to; }-*/;
+ public final native String message() /*-{ return this.message; }-*/;
+
+ public final Timestamp updated() {
+ String updatedRaw = updatedRaw();
+ return updatedRaw == null
+ ? null
+ : JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
+ }
+ private final native String updatedRaw() /*-{ return this.updated; }-*/;
+
+ public final native AccountInfo author() /*-{ return this.author; }-*/;
+
+ public final native boolean has_line() /*-{ return this.hasOwnProperty('line'); }-*/;
+
+ public final native CommentRange range() /*-{ return this.range; }-*/;
+
+ public final native void setRange(CommentRange range) /*-{ this.range = range; }-*/;
+
+ protected CommentInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInput.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInput.java
new file mode 100644
index 0000000..c96a67f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInput.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.diff.CommentRange;
+import com.google.gerrit.common.changes.Side;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
+
+import java.sql.Timestamp;
+
+public class CommentInput extends JavaScriptObject {
+ public static CommentInput create(CommentInfo original) {
+ CommentInput input = createObject().cast();
+ input.setId(original.id());
+ input.setPath(original.path());
+ input.setSide(original.side());
+ if (original.has_line()) {
+ input.setLine(original.line());
+ }
+ input.setRange(original.range());
+ input.setInReplyTo(original.in_reply_to());
+ input.setMessage(original.message());
+ return input;
+ }
+
+ public final native void setId(String id) /*-{ this.id = id; }-*/;
+ public final native void setPath(String path) /*-{ this.path = path; }-*/;
+
+ public final void setSide(Side side) {
+ setSideRaw(side.toString());
+ }
+ private final native void setSideRaw(String side) /*-{ this.side = side; }-*/;
+
+ public final native void setLine(int line) /*-{ this.line = line; }-*/;
+
+ public final native void setInReplyTo(String in_reply_to) /*-{
+ this.in_reply_to = in_reply_to;
+ }-*/;
+
+ public final native void setMessage(String message) /*-{ this.message = message; }-*/;
+ public final native String id() /*-{ return this.id; }-*/;
+ public final native String path() /*-{ return this.path; }-*/;
+
+ public final Side side() {
+ String s = sideRaw();
+ return s != null
+ ? Side.valueOf(s)
+ : Side.REVISION;
+ }
+ private final native String sideRaw() /*-{ return this.side }-*/;
+
+ public final native int line() /*-{ return this.line; }-*/;
+ public final native String in_reply_to() /*-{ return this.in_reply_to; }-*/;
+ public final native String message() /*-{ return this.message; }-*/;
+
+ public final Timestamp updated() {
+ return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
+ }
+ private final native String updatedRaw() /*-{ return this.updated; }-*/;
+
+ public final native boolean has_line() /*-{ return this.hasOwnProperty('line'); }-*/;
+
+ public final native CommentRange range() /*-{ return this.range; }-*/;
+
+ public final native void setRange(CommentRange range) /*-{ this.range = range; }-*/;
+
+ protected CommentInput() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
index 198480e..f612dcd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
@@ -46,7 +46,7 @@
interface Binder extends UiBinder<HTMLPanel, CommitMessageBlock> {
}
- private static Binder uiBinder = GWT.create(Binder.class);
+ private static final Binder uiBinder = GWT.create(Binder.class);
private KeyCommandSet keysAction;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
index 7b387ab..3002e48 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
@@ -19,10 +19,12 @@
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.changes.ListChangesOption;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.http.client.URL;
import java.util.ArrayList;
+import java.util.EnumSet;
import java.util.List;
import java.util.ListIterator;
@@ -103,6 +105,7 @@
finishDisplay();
}
},
+ EnumSet.noneOf(ListChangesOption.class),
queries.toArray(new String[queries.size()]));
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
index c21c68d..b2ccbcb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
@@ -71,6 +71,18 @@
.changeTablePagePrev(), prev));
keysNavigation.add(new DoLinkCommand(0, 'n', Util.C
.changeTablePageNext(), next));
+
+ keysNavigation.add(new DoLinkCommand(0, '[', Util.C
+ .changeTablePagePrev(), prev));
+ keysNavigation.add(new DoLinkCommand(0, ']', Util.C
+ .changeTablePageNext(), next));
+
+ keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadSearch()) {
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ Gerrit.display(getToken());
+ }
+ });
}
};
section = new ChangeTable2.Section();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index ca326cd..f1bb5b2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
@@ -13,21 +13,25 @@
// limitations under the License.
package com.google.gerrit.client.changes;
-
import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.GitwebLink;
import com.google.gerrit.client.download.DownloadPanel;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.ui.AccountLinkPanel;
-import com.google.gerrit.client.ui.CommentedActionDialog;
+import com.google.gerrit.client.ui.ActionDialog;
+import com.google.gerrit.client.ui.CherryPickDialog;
import com.google.gerrit.client.ui.ComplexDisclosurePanel;
import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.common.data.UiCommandDetail;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
@@ -35,18 +39,20 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.UserIdentity;
+import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.OpenEvent;
import com.google.gwt.event.logical.shared.OpenHandler;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DisclosurePanel;
import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwtjsonrpc.common.VoidResult;
@@ -174,6 +180,7 @@
if (changeDetail.isCurrentPatchSet(detail)) {
populateActions(detail);
}
+ populateCommands(detail);
}
if (detail.getPatchSet().isDraft()) {
if (changeDetail.canPublish()) {
@@ -383,6 +390,52 @@
actionsPanel.add(b);
}
+ if (changeDetail.canCherryPick()) {
+ final Button b = new Button(Util.C.buttonCherryPickChangeBegin());
+ b.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ b.setEnabled(false);
+ new CherryPickDialog(b, changeDetail.getChange().getProject()) {
+ {
+ sendButton.setText(Util.C.buttonCherryPickChangeSend());
+ if (changeDetail.getChange().getStatus().isClosed()) {
+ message.setText(Util.M.cherryPickedChangeDefaultMessage(
+ detail.getInfo().getMessage().trim(),
+ detail.getPatchSet().getRevision().get()));
+ } else {
+ message.setText(detail.getInfo().getMessage().trim());
+ }
+ }
+
+ @Override
+ public void onSend() {
+ ChangeApi.cherrypick(changeDetail.getChange().getChangeId(),
+ patchSet.getRevision().get(),
+ getDestinationBranch(),
+ getMessageText(),
+ new GerritCallback<ChangeInfo>() {
+ @Override
+ public void onSuccess(ChangeInfo result) {
+ sent = true;
+ Gerrit.display(PageLinks.toChange(new Change.Id(result
+ ._number())));
+ hide();
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ enableButtons(true);
+ super.onFailure(caught);
+ }
+ });
+ }
+ }.center();
+ }
+ });
+ actionsPanel.add(b);
+ }
+
if (changeDetail.canAbandon()) {
final Button b = new Button(Util.C.buttonAbandonChangeBegin());
b.addClickHandler(new ClickHandler() {
@@ -498,6 +551,47 @@
}
}
+ private void populateCommands(final PatchSetDetail detail) {
+ for (final UiCommandDetail cmd : detail.getCommands()) {
+ final Button b = new Button();
+ b.setText(cmd.label);
+ b.setEnabled(cmd.enabled);
+ b.setTitle(cmd.title);
+ b.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ b.setEnabled(false);
+ AsyncCallback<NativeString> cb =
+ new AsyncCallback<NativeString>() {
+ @Override
+ public void onFailure(Throwable caught) {
+ b.setEnabled(true);
+ new ErrorDialog(caught).center();
+ }
+
+ @Override
+ public void onSuccess(NativeString msg) {
+ b.setEnabled(true);
+ if (msg != null && !msg.asString().isEmpty()) {
+ Window.alert(msg.asString());
+ }
+ Gerrit.display(PageLinks.toChange(patchSet.getId()));
+ }
+ };
+ RestApi api = ChangeApi.revision(patchSet.getId()).view(cmd.id);
+ if ("PUT".equalsIgnoreCase(cmd.method)) {
+ api.put(JavaScriptObject.createObject(), cb);
+ } else if ("DELETE".equalsIgnoreCase(cmd.method)) {
+ api.delete(cb);
+ } else {
+ api.post(JavaScriptObject.createObject(), cb);
+ }
+ }
+ });
+ actionsPanel.add(b);
+ }
+ }
+
private void populateReviewAction() {
final Button b = new Button(Util.C.buttonReview());
b.addClickHandler(new ClickHandler() {
@@ -633,25 +727,4 @@
patchTable.setActive(active);
}
}
-
- private abstract class ActionDialog extends CommentedActionDialog<ChangeDetail> {
- public ActionDialog(final FocusWidget enableOnFailure, final boolean redirect,
- String dialogTitle, String dialogHeading) {
- super(dialogTitle, dialogHeading, new ChangeDetailCache.IgnoreErrorCallback() {
- @Override
- public void onSuccess(ChangeDetail result) {
- if (redirect) {
- Gerrit.display(PageLinks.toChange(result.getChange().getId()));
- } else {
- super.onSuccess(result);
- }
- }
-
- @Override
- public void onFailure(Throwable caught) {
- enableOnFailure.setEnabled(true);
- }
- });
- }
- }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
index 4e710db..6e9b6d6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
@@ -37,7 +37,6 @@
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
@@ -164,7 +163,7 @@
// Handled by ScreenLoadCallback.onFailure().
}
}));
- Util.DETAIL_SVC.patchSetPublishDetail(patchSetId, cbs.addGwtjsonrpc(
+ Util.DETAIL_SVC.patchSetPublishDetail(patchSetId, cbs.addFinal(
new ScreenLoadCallback<PatchSetPublishDetail>(this) {
@Override
protected void preDisplay(final PatchSetPublishDetail result) {
@@ -307,7 +306,7 @@
if (lastState != null && patchSetId.equals(lastState.patchSetId)
&& lastState.approvals.containsKey(label.name())) {
- b.setValue(lastState.approvals.get(label.name()) == value);
+ b.setValue(lastState.approvals.get(label.name()).equals(value));
} else {
b.setValue(b.parseValue() == (prior != null ? prior : 0));
}
@@ -406,7 +405,6 @@
private void onSend2(final boolean submit) {
ReviewInput data = ReviewInput.create();
data.message(ChangeApi.emptyToNull(message.getText().trim()));
- data.init();
for (final ValueRadioButton b : approvalButtons) {
if (b.getValue()) {
data.label(b.label.name(), b.parseValue());
@@ -436,23 +434,6 @@
});
}
- private static class ReviewInput extends JavaScriptObject {
- static ReviewInput create() {
- return (ReviewInput) createObject();
- }
-
- final native void message(String m) /*-{ if(m)this.message=m; }-*/;
- final native void label(String n, short v) /*-{ this.labels[n]=v; }-*/;
- final native void init() /*-{
- this.labels = {};
- this.strict_labels = true;
- this.drafts = 'PUBLISH';
- }-*/;
-
- protected ReviewInput() {
- }
- }
-
private void submit() {
ChangeApi.submit(patchSetId.getParentKey().get(), revision,
new GerritCallback<SubmitInfo>() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
index 01e294f..12bcf63 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
@@ -55,7 +55,7 @@
if (result.length() == 1 && isSingleQuery(query)) {
ChangeInfo c = result.get(0);
Change.Id id = c.legacy_id();
- Gerrit.display(PageLinks.toChange(id), new ChangeScreen(id));
+ Gerrit.display(PageLinks.toChange(id));
} else {
display(result);
QueryScreen.this.display();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInfo.java
new file mode 100644
index 0000000..3508c3d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInfo.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ReviewInfo extends JavaScriptObject {
+
+ public final native NativeMap<?> labels() /*-{ return this.labels }-*/;
+
+ protected ReviewInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
new file mode 100644
index 0000000..fa3d784
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ReviewInput extends JavaScriptObject {
+ public static enum NotifyHandling {
+ NONE, OWNER, OWNER_REVIEWERS, ALL;
+ }
+
+ public static ReviewInput create() {
+ ReviewInput r = createObject().cast();
+ r.init();
+ return r;
+ }
+
+ public final native void message(String m) /*-{ if(m)this.message=m; }-*/;
+ public final native void label(String n, short v) /*-{ this.labels[n]=v; }-*/;
+
+ public final void notify(NotifyHandling e) {
+ _notify(e.name());
+ }
+ private final native void _notify(String n) /*-{ this.notify=n; }-*/;
+
+ private final native void init() /*-{
+ this.labels = {};
+ this.strict_labels = true;
+ this.drafts = 'PUBLISH';
+ }-*/;
+
+ protected ReviewInput() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RevisionInfoCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RevisionInfoCache.java
new file mode 100644
index 0000000..53d618c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/RevisionInfoCache.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** Cache of PatchSet.Id to revision SHA-1 strings. */
+public class RevisionInfoCache {
+ private static final int LIMIT = 10;
+ private static final RevisionInfoCache IMPL = new RevisionInfoCache();
+
+ public static void add(Change.Id change, RevisionInfo info) {
+ IMPL.psToCommit.put(
+ new PatchSet.Id(change, info._number()),
+ info.name());
+ }
+
+ static String get(PatchSet.Id id) {
+ return IMPL.psToCommit.get(id);
+ }
+
+ private final LinkedHashMap<PatchSet.Id, String> psToCommit;
+
+ @SuppressWarnings("serial")
+ private RevisionInfoCache() {
+ psToCommit = new LinkedHashMap<PatchSet.Id, String>(LIMIT) {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<PatchSet.Id, String> e) {
+ return size() > LIMIT;
+ }
+ };
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java
index 70bf4b6..bd97790 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java
@@ -18,13 +18,13 @@
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import com.google.gwtjsonrpc.client.RemoteJsonException;
-class SubmitFailureDialog extends ErrorDialog {
- static boolean isConflict(Throwable err) {
+public class SubmitFailureDialog extends ErrorDialog {
+ public static boolean isConflict(Throwable err) {
return err instanceof RemoteJsonException
&& 409 == ((RemoteJsonException) err).getCode();
}
- SubmitFailureDialog(String msg) {
+ public SubmitFailureDialog(String msg) {
super(new SafeHtmlBuilder().append(msg.trim()).wikify());
setText(Util.C.submitFailed());
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
index a9206b7..6d8bc30 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
@@ -17,7 +17,7 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.core.client.JavaScriptObject;
-class SubmitInfo extends JavaScriptObject {
+public class SubmitInfo extends JavaScriptObject {
final Change.Status status() {
return Change.Status.valueOf(statusRaw());
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/CapabilityInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/CapabilityInfo.java
new file mode 100644
index 0000000..45abbd6
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/CapabilityInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.config;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class CapabilityInfo extends JavaScriptObject {
+ public final native String id() /*-{ return this.id; }-*/;
+ public final native String name() /*-{ return this.name; }-*/;
+
+ protected CapabilityInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
new file mode 100644
index 0000000..b283a0d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.config;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/**
+ * A collection of static methods which work on the Gerrit REST API for server
+ * configuration.
+ */
+public class ConfigServerApi {
+ /** map of the server wide capabilities (core & plugins). */
+ public static void capabilities(AsyncCallback<NativeMap<CapabilityInfo>> cb) {
+ new RestApi("/config/server/capabilities/").get(cb);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
new file mode 100644
index 0000000..bcc7a0c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java
@@ -0,0 +1,168 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.diff.PaddingManager.PaddingWidgetWrapper;
+import com.google.gerrit.client.diff.SidePanel.GutterWrapper;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOutHandler;
+import com.google.gwt.event.dom.client.MouseOverEvent;
+import com.google.gwt.event.dom.client.MouseOverHandler;
+import com.google.gwt.user.client.ui.Composite;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.Configuration;
+import net.codemirror.lib.TextMarker;
+import net.codemirror.lib.TextMarker.FromTo;
+
+/** An HtmlPanel for displaying a comment */
+abstract class CommentBox extends Composite {
+ static {
+ Resources.I.style().ensureInjected();
+ }
+
+ private PaddingManager widgetManager;
+ private PaddingWidgetWrapper selfWidgetWrapper;
+ private SideBySide2 parent;
+ private CodeMirror cm;
+ private DisplaySide side;
+ private DiffChunkInfo diffChunkInfo;
+ private GutterWrapper gutterWrapper;
+ private FromTo fromTo;
+ private TextMarker rangeMarker;
+ private TextMarker rangeHighlightMarker;
+
+ CommentBox(CodeMirror cm, CommentInfo info, DisplaySide side) {
+ this.cm = cm;
+ this.side = side;
+ CommentRange range = info.range();
+ if (range != null) {
+ fromTo = FromTo.create(range);
+ rangeMarker = cm.markText(
+ fromTo.getFrom(),
+ fromTo.getTo(),
+ Configuration.create()
+ .set("className", DiffTable.style.range()));
+ }
+ addDomHandler(new MouseOverHandler() {
+ @Override
+ public void onMouseOver(MouseOverEvent event) {
+ setRangeHighlight(true);
+ }
+ }, MouseOverEvent.getType());
+ addDomHandler(new MouseOutHandler() {
+ @Override
+ public void onMouseOut(MouseOutEvent event) {
+ setRangeHighlight(isOpen());
+ }
+ }, MouseOutEvent.getType());
+ }
+
+ @Override
+ protected void onLoad() {
+ resizePaddingWidget();
+ }
+
+ void resizePaddingWidget() {
+ if (!getCommentInfo().has_line()) {
+ return;
+ }
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ assert selfWidgetWrapper != null;
+ selfWidgetWrapper.getWidget().changed();
+ if (diffChunkInfo != null) {
+ parent.resizePaddingOnOtherSide(side, diffChunkInfo.getEnd());
+ } else {
+ assert widgetManager != null;
+ widgetManager.resizePaddingWidget();
+ }
+ }
+ });
+ }
+
+ abstract CommentInfo getCommentInfo();
+ abstract boolean isOpen();
+
+ void setOpen(boolean open) {
+ resizePaddingWidget();
+ setRangeHighlight(open);
+ }
+
+ PaddingManager getPaddingManager() {
+ return widgetManager;
+ }
+
+ void setPaddingManager(PaddingManager manager) {
+ widgetManager = manager;
+ }
+
+ void setSelfWidgetWrapper(PaddingWidgetWrapper wrapper) {
+ selfWidgetWrapper = wrapper;
+ }
+
+ PaddingWidgetWrapper getSelfWidgetWrapper() {
+ return selfWidgetWrapper;
+ }
+
+ void setDiffChunkInfo(DiffChunkInfo info) {
+ this.diffChunkInfo = info;
+ }
+
+ void setParent(SideBySide2 parent) {
+ this.parent = parent;
+ }
+
+ void setGutterWrapper(GutterWrapper wrapper) {
+ gutterWrapper = wrapper;
+ }
+
+ void setRangeHighlight(boolean highlight) {
+ if (fromTo != null) {
+ if (highlight && rangeHighlightMarker == null) {
+ rangeHighlightMarker = cm.markText(
+ fromTo.getFrom(),
+ fromTo.getTo(),
+ Configuration.create()
+ .set("className", DiffTable.style.rangeHighlight()));
+ } else if (!highlight && rangeHighlightMarker != null) {
+ rangeHighlightMarker.clear();
+ rangeHighlightMarker = null;
+ }
+ }
+ }
+
+ void clearRange() {
+ if (rangeMarker != null) {
+ rangeMarker.clear();
+ }
+ }
+
+ GutterWrapper getGutterWrapper() {
+ return gutterWrapper;
+ }
+
+ DisplaySide getSide() {
+ return side;
+ }
+
+ CodeMirror getCm() {
+ return cm;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
new file mode 100644
index 0000000..36f57b9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBoxUi.css
@@ -0,0 +1,81 @@
+/* Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.commentBox {
+ position: relative;
+ width: 679px;
+ min-height: 16px;
+ font-family: sans-serif;
+ background-color: #fcfa96;
+ border: 1px solid black;
+ -webkit-box-shadow: 3px 3px 3px #888888;
+ -moz-box-shadow: 3px 3px 3px #888888;
+ box-shadow: 3px 3px 3px #888888;
+ margin-bottom: 5px;
+ margin-right: 5px;
+}
+
+.header { cursor: pointer; }
+
+.summary {
+ color: #777;
+ position: absolute;
+ top: 1px;
+ left: 120px;
+ width: 408px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-bottom: 0.1em;
+}
+
+.date {
+ white-space: nowrap;
+ position: absolute;
+ top: 2px;
+ right: 5px;
+}
+
+.contents {
+ margin-left: 28px;
+ padding-top: 2px;
+ position: relative;
+}
+.contents p,
+.contents ul {
+ -webkit-margin-before: 0;
+ -webkit-margin-after: 0.3em;
+}
+
+.commentBox button {
+ margin-right: 3px;
+ margin-bottom: 1px;
+ padding: 1px;
+ text-align: center;
+ font-size: 8px;
+ font-weight: bold;
+ border: 1px solid black;
+ cursor: pointer;
+ color: #fff;
+ background-color: #4d90fe;
+ background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
+ -webkit-border-radius: 2px;
+ -webkit-box-sizing: content-box;
+}
+.commentBox button div {
+ width: 25px;
+ white-space: nowrap;
+ color: #fff;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
new file mode 100644
index 0000000..9887dbf
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+import net.codemirror.lib.LineCharacter;
+import net.codemirror.lib.TextMarker.FromTo;
+
+public class CommentRange extends JavaScriptObject {
+ public static CommentRange create(int sl, int sc, int el, int ec) {
+ CommentRange r = createObject().cast();
+ r.set(sl, sc, el, ec);
+ return r;
+ }
+
+ public static CommentRange create(FromTo fromTo) {
+ if (fromTo == null) {
+ return null;
+ }
+
+ LineCharacter from = fromTo.getFrom();
+ LineCharacter to = fromTo.getTo();
+ return create(
+ from.getLine() + 1, from.getCh(),
+ to.getLine() + 1, to.getCh());
+ }
+
+ public final native int start_line() /*-{ return this.start_line; }-*/;
+ public final native int start_character() /*-{ return this.start_character; }-*/;
+ public final native int end_line() /*-{ return this.end_line; }-*/;
+ public final native int end_character() /*-{ return this.end_character; }-*/;
+
+ private final native void set(int sl, int sc, int el, int ec) /*-{
+ this.start_line = sl;
+ this.start_character = sc;
+ this.end_line = el;
+ this.end_character = ec;
+ }-*/;
+
+ protected CommentRange() {
+ }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
new file mode 100644
index 0000000..826d477
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+public class DiffApi {
+ public enum IgnoreWhitespace {
+ NONE, TRAILING, CHANGED, ALL;
+ };
+
+ public static void list(int id, String revision,
+ AsyncCallback<NativeMap<FileInfo>> cb) {
+ ChangeApi.revision(id, revision)
+ .view("files")
+ .get(NativeMap.copyKeysIntoChildren("path", cb));
+ }
+
+ public static DiffApi diff(PatchSet.Id id, String path) {
+ return new DiffApi(ChangeApi.revision(id)
+ .view("files").id(path)
+ .view("diff"));
+ }
+
+ private final RestApi call;
+
+ private DiffApi(RestApi call) {
+ this.call = call;
+ }
+
+ public DiffApi base(PatchSet.Id id) {
+ if (id != null) {
+ call.addParameter("base", id.get());
+ }
+ return this;
+ }
+
+ public DiffApi ignoreWhitespace(AccountDiffPreference.Whitespace w) {
+ switch (w) {
+ default:
+ case IGNORE_NONE:
+ return ignoreWhitespace(IgnoreWhitespace.NONE);
+ case IGNORE_SPACE_AT_EOL:
+ return ignoreWhitespace(IgnoreWhitespace.TRAILING);
+ case IGNORE_SPACE_CHANGE:
+ return ignoreWhitespace(IgnoreWhitespace.CHANGED);
+ case IGNORE_ALL_SPACE:
+ return ignoreWhitespace(IgnoreWhitespace.ALL);
+ }
+ }
+
+ public DiffApi ignoreWhitespace(IgnoreWhitespace w) {
+ if (w != null && w != IgnoreWhitespace.NONE) {
+ call.addParameter("ignore-whitespace", w);
+ }
+ return this;
+ }
+
+ public DiffApi intraline(boolean intraline) {
+ if (intraline) {
+ call.addParameterTrue("intraline");
+ }
+ return this;
+ }
+
+ public DiffApi wholeFile() {
+ call.addParameter("context", "ALL");
+ return this;
+ }
+
+ public DiffApi context(int lines) {
+ call.addParameter("context", lines);
+ return this;
+ }
+
+ public void get(AsyncCallback<DiffInfo> cb) {
+ call.get(cb);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
new file mode 100644
index 0000000..9b3ac38
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffChunkInfo.java
@@ -0,0 +1,46 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+/** Object recording the position of a diff chunk and whether it's an edit */
+class DiffChunkInfo {
+ private DisplaySide side;
+ private int start;
+ private int end;
+ private boolean edit;
+
+ DiffChunkInfo(DisplaySide side, int start, int end, boolean edit) {
+ this.side = side;
+ this.start = start;
+ this.end = end;
+ this.edit = edit;
+ }
+
+ DisplaySide getSide() {
+ return side;
+ }
+
+ int getStart() {
+ return start;
+ }
+
+ int getEnd() {
+ return end;
+ }
+
+ boolean isEdit() {
+ return edit;
+ }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
new file mode 100644
index 0000000..f833509
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
@@ -0,0 +1,127 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+
+public class DiffInfo extends JavaScriptObject {
+ public static final String GITLINK = "x-git/gitlink";
+ public static final String SYMLINK = "x-git/symlink";
+
+ public final native FileMeta meta_a() /*-{ return this.meta_a; }-*/;
+ public final native FileMeta meta_b() /*-{ return this.meta_b; }-*/;
+ public final native JsArrayString diff_header() /*-{ return this.diff_header; }-*/;
+ public final native JsArray<Region> content() /*-{ return this.content; }-*/;
+
+ public final ChangeType change_type() {
+ return ChangeType.valueOf(change_typeRaw());
+ }
+ private final native String change_typeRaw()
+ /*-{ return this.change_type }-*/;
+
+ public final IntraLineStatus intraline_status() {
+ String s = intraline_statusRaw();
+ return s != null
+ ? IntraLineStatus.valueOf(s)
+ : IntraLineStatus.OFF;
+ }
+ private final native String intraline_statusRaw()
+ /*-{ return this.intraline_status }-*/;
+
+ public final boolean has_skip() {
+ JsArray<Region> c = content();
+ for (int i = 0; i < c.length(); i++) {
+ if (c.get(i).skip() != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public final String text_a() {
+ StringBuilder s = new StringBuilder();
+ JsArray<Region> c = content();
+ for (int i = 0; i < c.length(); i++) {
+ Region r = c.get(i);
+ if (r.ab() != null) {
+ append(s, r.ab());
+ } else if (r.a() != null) {
+ append(s, r.a());
+ }
+ // TODO skip may need to be handled
+ }
+ return s.toString();
+ }
+
+ public final String text_b() {
+ StringBuilder s = new StringBuilder();
+ JsArray<Region> c = content();
+ for (int i = 0; i < c.length(); i++) {
+ Region r = c.get(i);
+ if (r.ab() != null) {
+ append(s, r.ab());
+ } else if (r.b() != null) {
+ append(s, r.b());
+ }
+ // TODO skip may need to be handled
+ }
+ return s.toString();
+ }
+
+ private static void append(StringBuilder s, JsArrayString lines) {
+ for (int i = 0; i < lines.length(); i++) {
+ s.append(lines.get(i)).append('\n');
+ }
+ }
+
+ protected DiffInfo() {
+ }
+
+ public enum IntraLineStatus {
+ OFF, OK, TIMEOUT, FAILURE;
+ }
+
+ public static class FileMeta extends JavaScriptObject {
+ public final native String name() /*-{ return this.name; }-*/;
+ public final native String content_type() /*-{ return this.content_type; }-*/;
+
+ protected FileMeta() {
+ }
+ }
+
+ public static class Region extends JavaScriptObject {
+ public final native JsArrayString ab() /*-{ return this.ab; }-*/;
+ public final native JsArrayString a() /*-{ return this.a; }-*/;
+ public final native JsArrayString b() /*-{ return this.b; }-*/;
+ public final native int skip() /*-{ return this.skip || 0; }-*/;
+
+ public final native JsArray<Span> edit_a() /*-{ return this.edit_a }-*/;
+ public final native JsArray<Span> edit_b() /*-{ return this.edit_b }-*/;
+
+ protected Region() {
+ }
+ }
+
+ public static class Span extends JavaScriptObject {
+ public final native int skip() /*-{ return this[0]; }-*/;
+ public final native int mark() /*-{ return this[1]; }-*/;
+
+ protected Span() {
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
new file mode 100644
index 0000000..e736783
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
@@ -0,0 +1,151 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+
+/**
+ * A table with one row and two columns to hold the two CodeMirrors displaying
+ * the files to be diffed.
+ */
+class DiffTable extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, DiffTable> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ interface DiffTableStyle extends CssResource {
+ String intralineBg();
+ String diff();
+ String padding();
+ String activeLine();
+ String activeLineBg();
+ String hideNumber();
+ String range();
+ String rangeHighlight();
+ String showtabs();
+ }
+
+ @UiField
+ Element cmA;
+
+ @UiField
+ Element cmB;
+
+ @UiField
+ SidePanel sidePanel;
+
+ @UiField
+ Element patchSetNavRow;
+
+ @UiField
+ Element patchSetNavCellA;
+
+ @UiField
+ Element patchSetNavCellB;
+
+ @UiField(provided = true)
+ PatchSetSelectBox2 patchSetSelectBoxA;
+
+ @UiField(provided = true)
+ PatchSetSelectBox2 patchSetSelectBoxB;
+
+ @UiField
+ Element fileCommentRow;
+
+ @UiField
+ Element fileCommentCellA;
+
+ @UiField
+ Element fileCommentCellB;
+
+ @UiField(provided = true)
+ FileCommentPanel fileCommentPanelA;
+
+ @UiField(provided = true)
+ FileCommentPanel fileCommentPanelB;
+
+ @UiField
+ static DiffTableStyle style;
+
+ private SideBySide2 host;
+
+ DiffTable(SideBySide2 host, PatchSet.Id base, PatchSet.Id revision, String path) {
+ patchSetSelectBoxA = new PatchSetSelectBox2(
+ this, DisplaySide.A, revision.getParentKey(), base, path);
+ patchSetSelectBoxB = new PatchSetSelectBox2(
+ this, DisplaySide.B, revision.getParentKey(), revision, path);
+ PatchSetSelectBox2.link(patchSetSelectBoxA, patchSetSelectBoxB);
+ fileCommentPanelA = new FileCommentPanel(host, this, path, DisplaySide.A);
+ fileCommentPanelB = new FileCommentPanel(host, this, path, DisplaySide.B);
+ initWidget(uiBinder.createAndBindUi(this));
+ this.host = host;
+ }
+
+ @Override
+ protected void onLoad() {
+ updateFileCommentVisibility(false);
+ }
+
+ void updateFileCommentVisibility(boolean forceHide) {
+ UIObject.setVisible(patchSetNavRow, !forceHide);
+ if (forceHide || (fileCommentPanelA.getBoxCount() == 0 &&
+ fileCommentPanelB.getBoxCount() == 0)) {
+ UIObject.setVisible(fileCommentRow, false);
+ } else {
+ UIObject.setVisible(fileCommentRow, true);
+ }
+ host.resizeCodeMirror();
+ }
+
+ private FileCommentPanel getPanelFromSide(DisplaySide side) {
+ return side == DisplaySide.A ? fileCommentPanelA : fileCommentPanelB;
+ }
+
+ void createOrEditFileComment(DisplaySide side) {
+ getPanelFromSide(side).createOrEditFileComment();
+ updateFileCommentVisibility(false);
+ }
+
+ void addFileCommentBox(CommentBox box) {
+ getPanelFromSide(box.getSide()).addFileComment(box);
+ }
+
+ void onRemoveDraftBox(DraftBox box) {
+ getPanelFromSide(box.getSide()).onRemoveDraftBox(box);
+ }
+
+ int getHeaderHeight() {
+ return fileCommentRow.getOffsetHeight() + patchSetSelectBoxA.getOffsetHeight();
+ }
+
+ void setUpPatchSetNav(JsArray<RevisionInfo> list) {
+ patchSetSelectBoxA.setUpPatchSetNav(list);
+ patchSetSelectBoxB.setUpPatchSetNav(list);
+ }
+
+ void add(Widget widget) {
+ ((HTMLPanel) getWidget()).add(widget);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
new file mode 100644
index 0000000..070d10c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'
+ xmlns:d='urn:import:com.google.gerrit.client.diff'>
+ <ui:style type='com.google.gerrit.client.diff.DiffTable.DiffTableStyle'>
+ @external .CodeMirror, .CodeMirror-lines, .CodeMirror-selectedtext;
+ @external .CodeMirror-linenumber, .CodeMirror-vscrollbar;
+ @external .cm-keymap-fat-cursor, CodeMirror-cursor;
+ @external .cm-searching, .cm-trailingspace, .cm-tab;
+ .difftable .CodeMirror-lines {
+ padding: 0;
+ }
+ .difftable .CodeMirror pre {
+ padding: 0;
+ padding-bottom: 0.11em;
+ overflow: hidden;
+ }
+ .difftable .CodeMirror pre span {
+ padding-bottom: 0.11em;
+ }
+ .sidePanelCell {
+ width: 10px;
+ }
+ .table {
+ width: 100%;
+ table-layout: fixed;
+ border-spacing: 0;
+ }
+ .a, .b {
+ width: 50%;
+ }
+ .a .intralineBg,
+ .a .intralineBg .CodeMirror-linenumber {
+ background-color: #fee;
+ }
+ .b .intralineBg,
+ .b .intralineBg .CodeMirror-linenumber {
+ background-color: #dfd;
+ }
+ .a .diff,
+ .a .diff .CodeMirror-linenumber {
+ background-color: #faa;
+ }
+ .b .diff,
+ .b .diff .CodeMirror-linenumber {
+ background-color: #9f9;
+ }
+ .padding, .fileCommentRow {
+ background-color: #eee;
+ }
+ .fileCommentRow {
+ line-height: 1;
+ }
+ .fileCommentCell {
+ overflow-x: auto;
+ }
+ .a .CodeMirror-vscrollbar {
+ display: none !important;
+ }
+ .activeLine .CodeMirror-linenumber,
+ .activeLine .diff, .activeLine .intralineBg {
+ background-color: #E0FFFF !important;
+ }
+ .activeLineBg {
+ background-color: #E0FFFF !important;
+ }
+ .range {
+ background-color: #ffd500 !important;
+ }
+ .rangeHighlight {
+ background-color: #ffff00 !important;
+ }
+ .cm-searching {
+ background-color: #ffa !important;
+ }
+ .cm-trailingspace {
+ background-color: red !important;
+ }
+ .difftable .CodeMirror-selectedtext {
+ background-color: inherit !important;
+ }
+ .difftable .CodeMirror-linenumber {
+ height: 1.11em;
+ cursor: pointer;
+ }
+ .hideNumber .CodeMirror-linenumber {
+ color: transparent;
+ background-color: #def;
+ padding-right: 20px;
+ height: 1.3em;
+ }
+ .difftable .CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor {
+ opacity: 0.8;
+ z-index: 2;
+ }
+ .showtabs .cm-tab:before {
+ content: "\00bb";
+ color: #f00;
+ }
+ </ui:style>
+ <g:HTMLPanel styleName='{style.difftable}'>
+ <table class='{style.table}'>
+ <tr>
+ <td>
+ <table class='{style.table}'>
+ <tr ui:field='patchSetNavRow' class='{style.padding}'>
+ <td ui:field='patchSetNavCellA'>
+ <d:PatchSetSelectBox2 ui:field='patchSetSelectBoxA' />
+ </td>
+ <td ui:field='patchSetNavCellB'>
+ <d:PatchSetSelectBox2 ui:field='patchSetSelectBoxB' />
+ </td>
+ </tr>
+ <tr ui:field='fileCommentRow' class='{style.fileCommentRow}'>
+ <td ui:field='fileCommentCellA' class='{style.fileCommentCell}'>
+ <d:FileCommentPanel ui:field='fileCommentPanelA' />
+ </td>
+ <td ui:field='fileCommentCellB' class='{style.fileCommentCell}'>
+ <d:FileCommentPanel ui:field='fileCommentPanelB' />
+ </td>
+ </tr>
+ <tr>
+ <td ui:field='cmA' class='{style.a}'></td>
+ <td ui:field='cmB' class='{style.b}'></td>
+ </tr>
+ </table>
+ </td>
+ <td class='{style.sidePanelCell}'><d:SidePanel ui:field='sidePanel'/></td>
+ </tr>
+ </table>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java
new file mode 100644
index 0000000..c51a673
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DisplaySide.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+/** Enum representing the side on a side-by-side view */
+enum DisplaySide {
+ A, B;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
new file mode 100644
index 0000000..d54df3a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
@@ -0,0 +1,372 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.changes.CommentApi;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.changes.CommentInput;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.DoubleClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.MouseMoveEvent;
+import com.google.gwt.event.dom.client.MouseMoveHandler;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import net.codemirror.lib.CodeMirror;
+
+/** An HtmlPanel for displaying and editing a draft */
+class DraftBox extends CommentBox {
+ interface Binder extends UiBinder<HTMLPanel, DraftBox> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ private static final int INITIAL_LINES = 5;
+ private static final int MAX_LINES = 30;
+
+ private final SideBySide2 parent;
+ private final CommentLinkProcessor linkProcessor;
+ private final PatchSet.Id psId;
+ private CommentInfo comment;
+ private PublishedBox replyToBox;
+ private Timer expandTimer;
+
+ @UiField Widget header;
+ @UiField Element summary;
+ @UiField Element date;
+
+ @UiField Element p_view;
+ @UiField HTML message;
+ @UiField Button edit;
+ @UiField Button discard1;
+
+ @UiField Element p_edit;
+ @UiField NpTextArea editArea;
+ @UiField Button save;
+ @UiField Button cancel;
+ @UiField Button discard2;
+
+ DraftBox(
+ SideBySide2 sideBySide,
+ CodeMirror cm,
+ DisplaySide side,
+ CommentLinkProcessor clp,
+ PatchSet.Id id,
+ CommentInfo info) {
+ super(cm, info, side);
+
+ parent = sideBySide;
+ linkProcessor = clp;
+ psId = id;
+ initWidget(uiBinder.createAndBindUi(this));
+
+ expandTimer = new Timer() {
+ @Override
+ public void run() {
+ expandText();
+ }
+ };
+ set(info);
+
+ header.addDomHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ if (!isEdit()) {
+ setOpen(!isOpen());
+ }
+ }
+ }, ClickEvent.getType());
+ addDomHandler(new DoubleClickHandler() {
+ @Override
+ public void onDoubleClick(DoubleClickEvent event) {
+ if (isEdit()) {
+ editArea.setFocus(true);
+ } else {
+ setOpen(true);
+ setEdit(true);
+ }
+ }
+ }, DoubleClickEvent.getType());
+ addDomHandler(new MouseMoveHandler() {
+ @Override
+ public void onMouseMove(MouseMoveEvent event) {
+ resizePaddingWidget();
+ }
+ }, MouseMoveEvent.getType());
+ }
+
+ private void set(CommentInfo info) {
+ date.setInnerText(FormatUtil.shortFormatDayTime(info.updated()));
+ if (info.message() != null) {
+ String msg = info.message().trim();
+ summary.setInnerText(msg);
+ message.setHTML(linkProcessor.apply(
+ new SafeHtmlBuilder().append(msg).wikify()));
+ }
+ comment = info;
+ }
+
+ @Override
+ CommentInfo getCommentInfo() {
+ return comment;
+ }
+
+ @Override
+ boolean isOpen() {
+ return UIObject.isVisible(p_view);
+ }
+
+ @Override
+ void setOpen(boolean open) {
+ UIObject.setVisible(summary, !open);
+ UIObject.setVisible(p_view, open);
+ super.setOpen(open);
+ }
+
+ private void expandText() {
+ double cols = editArea.getCharacterWidth();
+ int rows = 2;
+ for (String line : editArea.getValue().split("\n")) {
+ rows += Math.ceil((1.0 + line.length()) / cols);
+ }
+ rows = Math.max(INITIAL_LINES, Math.min(rows, MAX_LINES));
+ if (editArea.getVisibleLines() != rows) {
+ editArea.setVisibleLines(rows);
+ }
+ resizePaddingWidget();
+ }
+
+ private boolean isEdit() {
+ return UIObject.isVisible(p_edit);
+ }
+
+ void setEdit(boolean edit) {
+ UIObject.setVisible(summary, false);
+ UIObject.setVisible(p_view, !edit);
+ UIObject.setVisible(p_edit, edit);
+
+ setRangeHighlight(edit);
+ if (edit) {
+ final String msg = comment.message() != null
+ ? comment.message().trim()
+ : "";
+ editArea.setValue(msg);
+ editArea.setFocus(true);
+ cancel.setVisible(!isNew());
+ expandText();
+ if (msg.length() > 0) {
+ Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
+ @Override
+ public boolean execute() {
+ editArea.setCursorPos(msg.length());
+ return false;
+ }
+ }, 0);
+ }
+ } else {
+ expandTimer.cancel();
+ }
+ resizePaddingWidget();
+ }
+
+ void registerReplyToBox(PublishedBox box) {
+ replyToBox = box;
+ }
+
+ @Override
+ protected void onUnload() {
+ expandTimer.cancel();
+ super.onUnload();
+ }
+
+ private void removeUI() {
+ if (replyToBox != null) {
+ replyToBox.unregisterReplyBox();
+ }
+ clearRange();
+ setRangeHighlight(false);
+ removeFromParent();
+ if (!getCommentInfo().has_line()) {
+ parent.removeFileCommentBox(this);
+ return;
+ }
+ PaddingManager manager = getPaddingManager();
+ manager.remove(this);
+ parent.removeDraft(this, comment.line() - 1);
+ getCm().focus();
+ getSelfWidgetWrapper().getWidget().clear();
+ getGutterWrapper().remove();
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ resizePaddingWidget();
+ }
+ });
+ }
+
+ @UiHandler("message")
+ void onMessageClick(ClickEvent e) {
+ e.stopPropagation();
+ }
+
+ @UiHandler("message")
+ void onMessageDoubleClick(DoubleClickEvent e) {
+ setEdit(true);
+ }
+
+ @UiHandler("edit")
+ void onEdit(ClickEvent e) {
+ e.stopPropagation();
+ setEdit(true);
+ }
+
+ @UiHandler("save")
+ void onSave(ClickEvent e) {
+ e.stopPropagation();
+ onSave();
+ }
+
+ private void onSave() {
+ String message = editArea.getValue().trim();
+ if (message.length() == 0) {
+ return;
+ }
+
+ CommentInfo original = comment;
+ CommentInput input = CommentInput.create(original);
+ input.setMessage(message);
+ enableEdit(false);
+
+ GerritCallback<CommentInfo> cb = new GerritCallback<CommentInfo>() {
+ @Override
+ public void onSuccess(CommentInfo result) {
+ enableEdit(true);
+ set(result);
+ if (result.message().length() < 70) {
+ UIObject.setVisible(p_edit, false);
+ setOpen(false);
+ } else {
+ setEdit(false);
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ enableEdit(true);
+ super.onFailure(e);
+ }
+ };
+ if (original.id() == null) {
+ CommentApi.createDraft(psId, input, cb);
+ } else {
+ CommentApi.updateDraft(psId, original.id(), input, cb);
+ }
+ getCm().focus();
+ }
+
+ private void enableEdit(boolean on) {
+ editArea.setEnabled(on);
+ save.setEnabled(on);
+ cancel.setEnabled(on);
+ discard2.setEnabled(on);
+ }
+
+ @UiHandler("cancel")
+ void onCancel(ClickEvent e) {
+ e.stopPropagation();
+ if (isNew() && !isDirty()) {
+ removeUI();
+ } else {
+ setEdit(false);
+ getCm().focus();
+ }
+ }
+
+ @UiHandler({"discard1", "discard2"})
+ void onDiscard(ClickEvent e) {
+ e.stopPropagation();
+ if (isNew()) {
+ removeUI();
+ } else {
+ setEdit(false);
+ CommentApi.deleteDraft(psId, comment.id(),
+ new GerritCallback<JavaScriptObject>() {
+ @Override
+ public void onSuccess(JavaScriptObject result) {
+ removeUI();
+ }
+ });
+ }
+ }
+
+ @UiHandler("editArea")
+ void onKeyDown(KeyDownEvent e) {
+ if ((e.isControlKeyDown() || e.isMetaKeyDown())
+ && !e.isAltKeyDown() && !e.isShiftKeyDown()) {
+ switch (e.getNativeKeyCode()) {
+ case 's':
+ case 'S':
+ e.preventDefault();
+ onSave();
+ return;
+ }
+ } else if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE && !isDirty()) {
+ if (isNew()) {
+ removeUI();
+ return;
+ } else {
+ setEdit(false);
+ getCm().focus();
+ return;
+ }
+ }
+ expandTimer.schedule(250);
+ }
+
+ private boolean isNew() {
+ return comment.id() == null;
+ }
+
+ private boolean isDirty() {
+ String msg = editArea.getValue().trim();
+ if (isNew()) {
+ return msg.length() > 0;
+ }
+ return !msg.equals(comment.message() != null
+ ? comment.message().trim()
+ : "");
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
new file mode 100644
index 0000000..52ef0ff
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:c='urn:import:com.google.gerrit.client'
+ xmlns:e='urn:import:com.google.gwtexpui.globalkey.client'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+ <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
+ <ui:style>
+ .draft {
+ width: 45px;
+ text-align: center;
+ color: #fff;
+ background-color: #aaa;
+ -webkit-border-radius: 2px;
+ }
+ .editArea { max-width: 637px; }
+ button.button div {
+ width: 35px;
+ }
+ button.discard {
+ color: #d14836;
+ background-color: #d14836;
+ background-image: -webkit-linear-gradient(top, #d14836, #d14836);
+ position: absolute;
+ left: 150px;
+ }
+ </ui:style>
+
+ <g:HTMLPanel styleName='{res.style.commentBox}'>
+ <div class='{res.style.contents}'>
+ <g:HTMLPanel ui:field='header' styleName='{res.style.header}'>
+ <div class='{style.draft}'>Draft</div>
+ <div ui:field='summary' class='{res.style.summary}'/>
+ <div ui:field='date' class='{res.style.date}'/>
+ </g:HTMLPanel>
+ <div ui:field='p_view' aria-hidden='true' style='display: NONE'>
+ <g:HTML ui:field='message' styleName=''/>
+ <div style='position: relative'>
+ <g:Button ui:field='edit'
+ title='Edit this draft comment'
+ styleName='{style.button}'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Edit</ui:msg></div>
+ </g:Button>
+ <g:Button ui:field='discard1'
+ title='Discard this draft comment'
+ styleName='{style.button}'
+ addStyleNames='{style.discard}'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Discard</ui:msg></div>
+ </g:Button>
+ </div>
+ </div>
+ <div ui:field='p_edit' aria-hidden='true' style='display: NONE'>
+ <e:NpTextArea ui:field='editArea'
+ characterWidth='60'
+ visibleLines='5'
+ spellCheck='true'
+ styleName='{style.editArea}'/>
+ <div style='position: relative'>
+ <g:Button ui:field='save'
+ title='Save this draft comment'
+ styleName='{style.button}'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Save</ui:msg></div>
+ </g:Button>
+ <g:Button ui:field='cancel' styleName='{style.button}'>
+ <div><ui:msg>Cancel</ui:msg></div>
+ </g:Button>
+ <g:Button ui:field='discard2'
+ title='Discard this draft comment'
+ styleName='{style.button}'
+ addStyleNames='{style.discard}'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Discard</ui:msg></div>
+ </g:Button>
+ </div>
+ </div>
+ </div>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/EditIterator.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/EditIterator.java
new file mode 100644
index 0000000..246c101
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/EditIterator.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.core.client.JsArrayString;
+
+import net.codemirror.lib.LineCharacter;
+
+/** An iterator for intraline edits */
+class EditIterator {
+ private final JsArrayString lines;
+ private final int startLine;
+ private int currLineIndex;
+ private int currLineOffset;
+
+ EditIterator(JsArrayString lineArray, int start) {
+ lines = lineArray;
+ startLine = start;
+ }
+
+ LineCharacter advance(int numOfChar) {
+ while (currLineIndex < lines.length()) {
+ int lengthWithNewline =
+ lines.get(currLineIndex).length() - currLineOffset + 1;
+ if (numOfChar < lengthWithNewline) {
+ LineCharacter at = LineCharacter.create(
+ startLine + currLineIndex,
+ numOfChar + currLineOffset);
+ currLineOffset += numOfChar;
+ return at;
+ }
+ numOfChar -= lengthWithNewline;
+ advanceLine();
+ if (numOfChar == 0) {
+ return LineCharacter.create(startLine + currLineIndex, 0);
+ }
+ }
+ throw new IllegalStateException("EditIterator index out of bound");
+ }
+
+ private void advanceLine() {
+ currLineIndex++;
+ currLineOffset = 0;
+ }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileCommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileCommentPanel.java
new file mode 100644
index 0000000..f252e65
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileCommentPanel.java
@@ -0,0 +1,84 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * HTMLPanel to hold file comments.
+ * TODO: Need to resize CodeMirror if this is resized since we don't have the
+ * system scrollbar.
+ */
+class FileCommentPanel extends Composite {
+
+ private SideBySide2 parent;
+ private DiffTable table;
+ private String path;
+ private DisplaySide side;
+ private List<CommentBox> boxes;
+ private FlowPanel body;
+
+ FileCommentPanel(SideBySide2 host, DiffTable table, String path, DisplaySide side) {
+ this.parent = host;
+ this.table = table;
+ this.path = path;
+ this.side = side;
+ boxes = new ArrayList<CommentBox>();
+ initWidget(body = new FlowPanel());
+ }
+
+ void createOrEditFileComment() {
+ if (!Gerrit.isSignedIn()) {
+ Gerrit.doSignIn(parent.getToken());
+ return;
+ }
+ if (boxes.isEmpty()) {
+ CommentInfo info = CommentInfo.createFile(
+ path,
+ parent.getStoredSideFromDisplaySide(side),
+ null,
+ null);
+ addFileComment(parent.addDraftBox(info, side));
+ } else {
+ CommentBox box = boxes.get(boxes.size() - 1);
+ if (box instanceof DraftBox) {
+ ((DraftBox) box).setEdit(true);
+ } else {
+ addFileComment(((PublishedBox) box).addReplyBox());
+ }
+ }
+ }
+
+ int getBoxCount() {
+ return boxes.size();
+ }
+
+ void addFileComment(CommentBox box) {
+ boxes.add(box);
+ body.add(box);
+ table.updateFileCommentVisibility(false);
+ }
+
+ void onRemoveDraftBox(DraftBox box) {
+ boxes.remove(box);
+ table.updateFileCommentVisibility(false);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
new file mode 100644
index 0000000..4e88caf
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+
+import java.util.Collections;
+import java.util.Comparator;
+
+public class FileInfo extends JavaScriptObject {
+ public final native String path() /*-{ return this.path; }-*/;
+ public final native String old_path() /*-{ return this.old_path; }-*/;
+ public final native int lines_inserted() /*-{ return this.lines_inserted || 0; }-*/;
+ public final native int lines_deleted() /*-{ return this.lines_deleted || 0; }-*/;
+ public final native boolean binary() /*-{ return this.binary || false; }-*/;
+
+ public final native int _row() /*-{ return this._row }-*/;
+ public final native void _row(int r) /*-{ this._row = r }-*/;
+
+ public static void sortFileInfoByPath(JsArray<FileInfo> list) {
+ Collections.sort(Natives.asList(list), new Comparator<FileInfo>() {
+ @Override
+ public int compare(FileInfo a, FileInfo b) {
+ if (Patch.COMMIT_MSG.equals(a.path())) {
+ return -1;
+ } else if (Patch.COMMIT_MSG.equals(b.path())) {
+ return 1;
+ }
+ return a.path().compareTo(b.path());
+ }
+ });
+ }
+
+ public static String getFileName(String path) {
+ String fileName = Patch.COMMIT_MSG.equals(path)
+ ? Util.C.commitMessage()
+ : path;
+ int s = fileName.lastIndexOf('/');
+ return s >= 0 ? fileName.substring(s + 1) : fileName;
+ }
+
+ protected FileInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
new file mode 100644
index 0000000..c46762c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
@@ -0,0 +1,205 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ReviewInfo;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Visibility;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtorm.client.KeyUtil;
+
+class Header extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, Header> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ @UiField CheckBox reviewed;
+ @UiField Element filePath;
+
+ @UiField InlineHyperlink prev;
+ @UiField InlineHyperlink up;
+ @UiField InlineHyperlink next;
+
+ private final KeyCommandSet keys;
+ private final PatchSet.Id patchSetId;
+ private final String path;
+ private boolean hasPrev;
+ private boolean hasNext;
+ private String nextPath;
+
+ Header(KeyCommandSet keys, PatchSet.Id patchSetId, String path) {
+ initWidget(uiBinder.createAndBindUi(this));
+ this.keys = keys;
+ this.patchSetId = patchSetId;
+ this.path = path;
+
+ SafeHtml.setInnerHTML(filePath, formatPath(path));
+ up.setTargetHistoryToken(PageLinks.toChange2(
+ patchSetId.getParentKey(),
+ String.valueOf(patchSetId.get())));
+ }
+
+ private static SafeHtml formatPath(String path) {
+ SafeHtmlBuilder b = new SafeHtmlBuilder();
+ if (Patch.COMMIT_MSG.equals(path)) {
+ return b.append(Util.C.commitMessage());
+ }
+
+ int s = path.lastIndexOf('/') + 1;
+ b.append(path.substring(0, s));
+ b.openElement("b");
+ b.append(path.substring(s));
+ b.closeElement("b");
+ return b;
+ }
+
+ @Override
+ protected void onLoad() {
+ ChangeApi.revision(patchSetId).view("files").get(
+ new GerritCallback<NativeMap<FileInfo>>() {
+ @Override
+ public void onSuccess(NativeMap<FileInfo> result) {
+ result.copyKeysIntoChildren("path");
+ JsArray<FileInfo> files = result.values();
+ FileInfo.sortFileInfoByPath(files);
+ int index = 0; // TODO: Maybe use patchIndex.
+ for (int i = 0; i < files.length(); i++) {
+ if (path.equals(files.get(i).path())) {
+ index = i;
+ break;
+ }
+ }
+ FileInfo nextInfo = index == files.length() - 1
+ ? null
+ : files.get(index + 1);
+ setupNav(prev, '[', PatchUtil.C.previousFileHelp(),
+ index == 0 ? null : files.get(index - 1));
+ setupNav(next, ']', PatchUtil.C.nextFileHelp(), nextInfo);
+ nextPath = nextInfo != null ? nextInfo.path() : null;
+ }
+ });
+
+ if (Gerrit.isSignedIn()) {
+ ChangeApi.revision(patchSetId).view("files")
+ .addParameterTrue("reviewed")
+ .get(new AsyncCallback<JsArrayString>() {
+ @Override
+ public void onSuccess(JsArrayString result) {
+ for (int i = 0; i < result.length(); i++) {
+ if (path.equals(result.get(i))) {
+ reviewed.setValue(true, false);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ }
+ });
+ }
+ }
+
+ void setReviewed(boolean r) {
+ reviewed.setValue(r, true);
+ }
+
+ boolean isReviewed() {
+ return reviewed.getValue();
+ }
+
+ @UiHandler("reviewed")
+ void onValueChange(ValueChangeEvent<Boolean> event) {
+ RestApi api = ChangeApi.revision(patchSetId)
+ .view("files")
+ .id(path)
+ .view("reviewed");
+ if (event.getValue()) {
+ api.put(CallbackGroup.<ReviewInfo>emptyCallback());
+ } else {
+ api.delete(CallbackGroup.<ReviewInfo>emptyCallback());
+ }
+ }
+
+ private String url(FileInfo info) {
+ Change.Id c = patchSetId.getParentKey();
+ StringBuilder p = new StringBuilder();
+ p.append("/c/").append(c).append('/');
+ p.append(patchSetId.get()).append('/').append(KeyUtil.encode(info.path()));
+ p.append(info.binary() ? ",unified" : ",cm");
+ return p.toString();
+ }
+
+ private void setupNav(InlineHyperlink link, int key, String help, FileInfo info) {
+ if (info != null) {
+ final String url = url(info);
+ link.setTargetHistoryToken(url);
+ link.setTitle(FileInfo.getFileName(info.path()));
+ keys.add(new KeyCommand(0, key, help) {
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ Gerrit.display(url);
+ }
+ });
+ if (link == prev) {
+ hasPrev = true;
+ } else {
+ hasNext = true;
+ }
+ } else {
+ link.getElement().getStyle().setVisibility(Visibility.HIDDEN);
+ keys.add(new UpToChangeCommand2(patchSetId, 0, key));
+ }
+ }
+
+ boolean hasPrev() {
+ return hasPrev;
+ }
+
+ boolean hasNext() {
+ return hasNext;
+ }
+
+ String getNextPath() {
+ return nextPath;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
new file mode 100644
index 0000000..18de3d5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'
+ xmlns:x='urn:import:com.google.gerrit.client.ui'>
+ <ui:style>
+ .header {
+ position: relative;
+ }
+ .reviewed input {
+ margin: 0;
+ padding: 0;
+ vertical-align: bottom;
+ }
+ .path {
+ }
+ .navigation {
+ position: absolute;
+ top: 0;
+ right: 15px;
+ font-family: Arial Unicode MS, sans-serif;
+ }
+ </ui:style>
+ <g:HTMLPanel styleName='{style.header}'>
+ <g:CheckBox ui:field='reviewed'
+ styleName='{style.reviewed}'
+ title='Mark file as reviewed (Shortcut: r)'>
+ <ui:attribute name='title'/>
+ </g:CheckBox>
+ <span ui:field='filePath' class='{style.path}'/>
+
+ <div class='{style.navigation}'>
+ <x:InlineHyperlink ui:field='prev'>⇦</x:InlineHyperlink>
+ <x:InlineHyperlink ui:field='up' title='Up to change (Shortcut: u)'>
+ <ui:attribute name='title'/>
+ ⇧
+ </x:InlineHyperlink>
+ <x:InlineHyperlink ui:field='next'>⇨</x:InlineHyperlink>
+ </div>
+ </g:HTMLPanel>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
new file mode 100644
index 0000000..7a5ce5d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/LineMapper.java
@@ -0,0 +1,184 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Helper class to handle calculations involving line gaps. */
+class LineMapper {
+ private int lineA;
+ private int lineB;
+ private List<LineGap> lineMapAtoB;
+ private List<LineGap> lineMapBtoA;
+
+ LineMapper() {
+ lineMapAtoB = new ArrayList<LineGap>();
+ lineMapBtoA = new ArrayList<LineGap>();
+ }
+
+ int getLineA() {
+ return lineA;
+ }
+
+ int getLineB() {
+ return lineB;
+ }
+
+ void appendCommon(int numLines) {
+ lineA += numLines;
+ lineB += numLines;
+ }
+
+ void appendInsert(int numLines) {
+ int origLineB = lineB;
+ lineB += numLines;
+ int bAheadOfA = lineB - lineA;
+ lineMapAtoB.add(new LineGap(lineA, -1, bAheadOfA));
+ lineMapBtoA.add(new LineGap(origLineB, lineB - 1, -bAheadOfA));
+ }
+
+ void appendDelete(int numLines) {
+ int origLineA = lineA;
+ lineA += numLines;
+ int aAheadOfB = lineA - lineB;
+ lineMapAtoB.add(new LineGap(origLineA, lineA - 1, -aAheadOfB));
+ lineMapBtoA.add(new LineGap(lineB, -1, aAheadOfB));
+ }
+
+ /**
+ * Helper method to retrieve the line number on the other side.
+ *
+ * Given a line number on one side, performs a binary search in the lineMap
+ * to find the corresponding LineGap record.
+ *
+ * A LineGap records gap information from the start of an actual gap up to
+ * the start of the next gap. In the following example,
+ * lineMapAtoB will have LineGap: {start: 1, end: -1, delta: 3}
+ * (end set to -1 to represent a dummy gap of length zero. The binary search
+ * only looks at start so setting it to -1 has no effect here.)
+ * lineMapBtoA will have LineGap: {start: 1, end: 3, delta: -3}
+ * These LineGaps control lines between 1 and 5.
+ *
+ * The "delta" is computed as the number to add on our side to get the line
+ * number on the other side given a line after the actual gap, so the result
+ * will be (line + delta). All lines within the actual gap (1 to 3) are
+ * considered corresponding to the last line above the region on the other
+ * side, which is 0 in this case. For these lines, we do (end + delta).
+ *
+ * For example, to get the line number on the left corresponding to 1 on the
+ * right (lineOnOther(REVISION, 1)), the method looks up in lineMapBtoA,
+ * finds the "delta" to be -3, and returns 3 + (-3) = 0 since 1 falls in the
+ * actual gap. On the other hand, the line corresponding to 5 on the right
+ * will be 5 + (-3) = 2, since 5 is in the region after the gap (but still
+ * controlled by the current LineGap).
+ *
+ * PARENT REVISION
+ * 0 | 0
+ * - | 1 \ \
+ * - | 2 | Actual insertion gap |
+ * - | 3 / | Region controlled by one LineGap
+ * 1 | 4 <- delta = 4 - 1 = 3 |
+ * 2 | 5 /
+ * - | 6
+ * ...
+ */
+ LineOnOtherInfo lineOnOther(DisplaySide mySide, int line) {
+ List<LineGap> lineGaps = mySide == DisplaySide.A ? lineMapAtoB : lineMapBtoA;
+ // Create a dummy LineGap for the search.
+ int ret = Collections.binarySearch(lineGaps, new LineGap(line));
+ if (ret == -1) {
+ return new LineOnOtherInfo(line, true);
+ } else {
+ LineGap lookup = lineGaps.get(0 <= ret ? ret : -ret - 2);
+ int start = lookup.start;
+ int end = lookup.end;
+ int delta = lookup.delta;
+ if (start <= line && line <= end && end != -1) { // Line falls within gap
+ return new LineOnOtherInfo(end + delta, false);
+ } else { // Line after gap
+ return new LineOnOtherInfo(line + delta, true);
+ }
+ }
+ }
+
+ /**
+ * @field line The line number on the other side.
+ * @field aligned Whether the two lines are at the same height when displayed.
+ */
+ static class LineOnOtherInfo {
+ private int line;
+ private boolean aligned;
+
+ LineOnOtherInfo(int line, boolean aligned) {
+ this.line = line;
+ this.aligned = aligned;
+ }
+
+ int getLine() {
+ return line;
+ }
+
+ boolean isAligned() {
+ return aligned;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof LineOnOtherInfo) {
+ LineOnOtherInfo other = (LineOnOtherInfo) obj;
+ return aligned == other.aligned && line == other.line;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return line + " " + aligned;
+ }
+ }
+
+ /**
+ * Helper class to record line gap info and assist in calculation of line
+ * number on the other side.
+ *
+ * For a mapping from A to B, where A is the side with an insertion:
+ * @field start The start line of the insertion in A.
+ * @field end The exclusive end line of the insertion in A.
+ * @field delta The offset added to A to get the line number in B calculated
+ * from end.
+ */
+ private static class LineGap implements Comparable<LineGap> {
+ private final int start;
+ private final int end;
+ private final int delta;
+
+ private LineGap(int start, int end, int delta) {
+ this.start = start;
+ this.end = end;
+ this.delta = delta;
+ }
+
+ private LineGap(int line) {
+ this(line, 0, 0);
+ }
+
+ @Override
+ public int compareTo(LineGap o) {
+ return start - o.start;
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java
new file mode 100644
index 0000000..c22769e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/NoOpKeyCommand.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+
+/** A KeyCommand that does nothing, used to display a help message */
+class NoOpKeyCommand extends KeyCommand {
+ NoOpKeyCommand(int mask, int key, String help) {
+ super(mask, key, help);
+ }
+
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PaddingManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PaddingManager.java
new file mode 100644
index 0000000..dc212e5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PaddingManager.java
@@ -0,0 +1,169 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.dom.client.Element;
+
+import net.codemirror.lib.LineWidget;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages paddings for CommentBoxes. Each line that may need to be padded owns
+ * a PaddingManager instance, which maintains a padding widget whose height
+ * changes as necessary. PaddingManager calculates padding by taking the
+ * difference of the sum of CommentBox heights on the two sides.
+ *
+ * Note that in the case of an insertion or deletion gap, A PaddingManager
+ * can map to a list of managers on the other side. The padding needed is then
+ * calculated from the sum of all their heights.
+ *
+ * TODO: Let PaddingManager also take care of the paddings introduced by
+ * insertions and deletions.
+ */
+class PaddingManager {
+ private List<CommentBox> comments;
+ private PaddingWidgetWrapper wrapper;
+ private List<PaddingManager> others;
+
+ PaddingManager(PaddingWidgetWrapper padding) {
+ comments = new ArrayList<CommentBox>();
+ others = new ArrayList<PaddingManager>();
+ this.wrapper = padding;
+ }
+
+ static void link(PaddingManager a, PaddingManager b) {
+ if (!a.others.contains(b)) {
+ a.others.add(b);
+ }
+ if (!b.others.contains(a)) {
+ b.others.add(a);
+ }
+ }
+
+ private int getMyTotalHeight() {
+ int total = 0;
+ for (CommentBox box : comments) {
+ /**
+ * This gets the height of CM's line widget div, taking the margin and
+ * the horizontal scrollbar into account.
+ */
+ total += box.getSelfWidgetWrapper().getElement().getParentElement().getOffsetHeight();
+ }
+ return total;
+ }
+
+ /**
+ * If this instance is on the insertion side, its counterpart on the other
+ * side will map to a group of PaddingManagers on this side, so we calculate
+ * the group's total height instead of an individual one's.
+ */
+ private int getGroupTotalHeight() {
+ if (others.size() > 1) {
+ return getMyTotalHeight();
+ } else {
+ return others.get(0).getOthersTotalHeight();
+ }
+ }
+
+ private int getOthersTotalHeight() {
+ int total = 0;
+ for (PaddingManager manager : others) {
+ total += manager.getMyTotalHeight();
+ }
+ return total;
+ }
+
+ private void setPaddingHeight(int height) {
+ SideBySide2.setHeightInPx(wrapper.element, height);
+ wrapper.widget.changed();
+ }
+
+ void resizePaddingWidget() {
+ if (others.isEmpty()) {
+ return;
+ }
+ int myHeight = getGroupTotalHeight();
+ int othersHeight = getOthersTotalHeight();
+ int paddingNeeded = othersHeight - myHeight;
+ if (paddingNeeded < 0) {
+ for (PaddingManager manager : others.get(0).others) {
+ manager.setPaddingHeight(0);
+ }
+ others.get(others.size() - 1).setPaddingHeight(-paddingNeeded);
+ } else {
+ setPaddingHeight(paddingNeeded);
+ for (PaddingManager other : others) {
+ other.setPaddingHeight(0);
+ }
+ }
+ }
+
+ /** This is unused now because threading info is ignored. */
+ int getReplyIndex(CommentBox box) {
+ return comments.indexOf(box) + 1;
+ }
+
+ int getCurrentCount() {
+ return comments.size();
+ }
+
+ void insert(CommentBox box, int index) {
+ comments.add(index, box);
+ }
+
+ void remove(CommentBox box) {
+ comments.remove(box);
+ }
+
+ static class PaddingWidgetWrapper {
+ private LineWidget widget;
+ private Element element;
+
+ PaddingWidgetWrapper(LineWidget w, Element e) {
+ widget = w;
+ element = e;
+ }
+
+ LineWidget getWidget() {
+ return widget;
+ }
+
+ Element getElement() {
+ return element;
+ }
+ }
+
+ static class LinePaddingWidgetWrapper extends PaddingWidgetWrapper {
+ private int chunkLength;
+ private int otherLine;
+
+ LinePaddingWidgetWrapper(PaddingWidgetWrapper pair, int otherLine, int chunkLength) {
+ super(pair.widget, pair.element);
+
+ this.otherLine = otherLine;
+ this.chunkLength = chunkLength;
+ }
+
+ int getChunkLength() {
+ return chunkLength;
+ }
+
+ int getOtherLine() {
+ return otherLine;
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java
new file mode 100644
index 0000000..d258735
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.java
@@ -0,0 +1,117 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Image;
+
+/**
+ * HTMLPanel to select among patch sets
+ * TODO: Implement download link.
+ */
+class PatchSetSelectBox2 extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, PatchSetSelectBox2> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ interface BoxStyle extends CssResource {
+ String selected();
+ }
+
+ @UiField Image icon;
+ @UiField HTMLPanel linkPanel;
+ @UiField BoxStyle style;
+
+ private DiffTable table;
+ private DisplaySide side;
+ private boolean sideA;
+ private String path;
+ private Change.Id changeId;
+ private PatchSet.Id revision;
+ private PatchSet.Id idActive;
+ private PatchSetSelectBox2 other;
+
+ PatchSetSelectBox2(DiffTable table, final DisplaySide side,
+ final Change.Id changeId, final PatchSet.Id revision, String path) {
+ initWidget(uiBinder.createAndBindUi(this));
+ icon.setTitle(PatchUtil.C.addFileCommentToolTip());
+ icon.addStyleName(Gerrit.RESOURCES.css().link());
+ this.table = table;
+ this.side = side;
+ this.sideA = side == DisplaySide.A;
+ this.changeId = changeId;
+ this.revision = revision;
+ this.idActive = (sideA && revision == null) ? null : revision;
+ this.path = path;
+ }
+
+ void setUpPatchSetNav(JsArray<RevisionInfo> list) {
+ InlineHyperlink baseLink = null;
+ InlineHyperlink selectedLink = null;
+ if (sideA) {
+ baseLink = createLink(PatchUtil.C.patchBase(), null);
+ linkPanel.add(baseLink);
+ }
+ for (int i = 0; i < list.length(); i++) {
+ RevisionInfo r = list.get(i);
+ InlineHyperlink link = createLink(
+ String.valueOf(r._number()), new PatchSet.Id(changeId, r._number()));
+ linkPanel.add(link);
+ if (revision != null && r._number() == revision.get()) {
+ selectedLink = link;
+ }
+ }
+ if (selectedLink != null) {
+ selectedLink.setStyleName(style.selected());
+ } else if (sideA) {
+ baseLink.setStyleName(style.selected());
+ }
+ }
+
+ static void link(PatchSetSelectBox2 a, PatchSetSelectBox2 b) {
+ a.other = b;
+ b.other = a;
+ }
+
+ private InlineHyperlink createLink(String label, PatchSet.Id id) {
+ assert other != null;
+ if (sideA) {
+ assert other.idActive != null;
+ }
+ return new InlineHyperlink(label, Dispatcher.toPatchSideBySide2(
+ sideA ? id : other.idActive,
+ sideA ? other.idActive : id,
+ path));
+ }
+
+ @UiHandler("icon")
+ void onIconClick(ClickEvent e) {
+ table.createOrEditFileComment(side);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.ui.xml
new file mode 100644
index 0000000..dca0cd5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox2.ui.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+ <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
+ <ui:with field='patchConstants'
+ type='com.google.gerrit.client.patches.PatchConstants'/>
+ <ui:style type='com.google.gerrit.client.diff.PatchSetSelectBox2.BoxStyle'>
+ @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+ .table {
+ width: 100%;
+ }
+ .linkCell {
+ text-align: center;
+ font-size: 12px;
+ white-space: normal;
+ font-family: sans-serif;
+ font-weight: bold;
+ }
+ .linkCell div {
+ padding-left: 3px;
+ padding-right: 3px;
+ vertical-align: middle;
+ display: inline-block;
+ }
+ .linkCell a {
+ padding-left: 3px;
+ padding-right: 3px;
+ text-decoration: none;
+ vertical-align: middle;
+ display: inline-block;
+ }
+ .selected {
+ font-weight: bold;
+ background-color: selectionColor;
+ }
+ .hidden {
+ visibility: hidden;
+ }
+ .iconCell {
+ width: 16px;
+ }
+ </ui:style>
+ <g:HTMLPanel>
+ <table class='{style.table}'>
+ <td class='{style.iconCell}'>
+ <g:Image ui:field='icon' resource='{res.addFileComment}'/>
+ </td>
+ <td class='{style.linkCell}'>
+ <g:HTMLPanel ui:field='linkPanel'>
+ <g:Label>
+ <ui:text from='{patchConstants.patchSet}'/>
+ </g:Label>
+ </g:HTMLPanel>
+ </td>
+ </table>
+ </g:HTMLPanel>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
new file mode 100644
index 0000000..f1ea38b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java
@@ -0,0 +1,207 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.AvatarImage;
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.CommentApi;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.changes.CommentInput;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import net.codemirror.lib.CodeMirror;
+
+/** An HtmlPanel for displaying a published comment */
+class PublishedBox extends CommentBox {
+ interface Binder extends UiBinder<HTMLPanel, PublishedBox> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ static interface Style extends CssResource {
+ String closed();
+ }
+
+ private final SideBySide2 parent;
+ private final PatchSet.Id psId;
+ private final CommentInfo comment;
+ private DraftBox replyBox;
+
+ @UiField Style style;
+ @UiField Widget header;
+ @UiField Element name;
+ @UiField Element summary;
+ @UiField Element date;
+ @UiField Element message;
+ @UiField Element buttons;
+ @UiField Button reply;
+ @UiField Button done;
+
+ @UiField(provided = true)
+ AvatarImage avatar;
+
+ PublishedBox(
+ SideBySide2 parent,
+ CodeMirror cm,
+ DisplaySide side,
+ CommentLinkProcessor clp,
+ PatchSet.Id psId,
+ CommentInfo info) {
+ super(cm, info, side);
+
+ this.parent = parent;
+ this.psId = psId;
+ this.comment = info;
+
+ if (info.author() != null) {
+ avatar = new AvatarImage(info.author());
+ avatar.setSize("", "");
+ } else {
+ avatar = new AvatarImage();
+ }
+
+ initWidget(uiBinder.createAndBindUi(this));
+ header.addDomHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ setOpen(!isOpen());
+ }
+ }, ClickEvent.getType());
+
+ name.setInnerText(authorName(info));
+ date.setInnerText(FormatUtil.shortFormatDayTime(info.updated()));
+ if (info.message() != null) {
+ String msg = info.message().trim();
+ summary.setInnerText(msg);
+ message.setInnerSafeHtml(clp.apply(
+ new SafeHtmlBuilder().append(msg).wikify()));
+ }
+ }
+
+ @Override
+ CommentInfo getCommentInfo() {
+ return comment;
+ }
+
+ @Override
+ boolean isOpen() {
+ return UIObject.isVisible(message);
+ }
+
+ void setOpen(boolean open) {
+ UIObject.setVisible(summary, !open);
+ UIObject.setVisible(message, open);
+ UIObject.setVisible(buttons, open);
+ if (open) {
+ removeStyleName(style.closed());
+ } else {
+ addStyleName(style.closed());
+ }
+ super.setOpen(open);
+ }
+
+ void registerReplyBox(DraftBox box) {
+ replyBox = box;
+ box.registerReplyToBox(this);
+ }
+
+ void unregisterReplyBox() {
+ replyBox = null;
+ }
+
+ private void openReplyBox() {
+ replyBox.setOpen(true);
+ replyBox.setEdit(true);
+ }
+
+ DraftBox addReplyBox() {
+ DraftBox box = parent.addDraftBox(parent.createReply(comment), getSide());
+ registerReplyBox(box);
+ return box;
+ }
+
+ void doReply() {
+ if (!Gerrit.isSignedIn()) {
+ Gerrit.doSignIn(parent.getToken());
+ } else if (replyBox == null) {
+ DraftBox box = addReplyBox();
+ if (!getCommentInfo().has_line()) {
+ parent.addFileCommentBox(box);
+ }
+ } else {
+ openReplyBox();
+ }
+ }
+
+ @UiHandler("reply")
+ void onReply(ClickEvent e) {
+ e.stopPropagation();
+ doReply();
+ }
+
+ @UiHandler("done")
+ void onReplyDone(ClickEvent e) {
+ e.stopPropagation();
+ if (!Gerrit.isSignedIn()) {
+ Gerrit.doSignIn(parent.getToken());
+ } else if (replyBox == null) {
+ done.setEnabled(false);
+ CommentInput input = CommentInput.create(parent.createReply(comment));
+ input.setMessage(PatchUtil.C.cannedReplyDone());
+ CommentApi.createDraft(psId, input,
+ new GerritCallback<CommentInfo>() {
+ @Override
+ public void onSuccess(CommentInfo result) {
+ done.setEnabled(true);
+ setOpen(false);
+ DraftBox box = parent.addDraftBox(result, getSide());
+ registerReplyBox(box);
+ if (!getCommentInfo().has_line()) {
+ parent.addFileCommentBox(box);
+ }
+ }
+ });
+ } else {
+ openReplyBox();
+ setOpen(false);
+ }
+ }
+
+ private static String authorName(CommentInfo info) {
+ if (info.author() != null) {
+ if (info.author().name() != null) {
+ return info.author().name();
+ }
+ return Gerrit.getConfig().getAnonymousCowardName();
+ }
+ return Util.C.messageNoAuthor();
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
new file mode 100644
index 0000000..f3beda1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder
+ xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:c='urn:import:com.google.gerrit.client'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+ <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
+ <ui:style type='com.google.gerrit.client.diff.PublishedBox.Style'>
+ .avatar {
+ position: absolute;
+ width: 26px;
+ height: 26px;
+ }
+ .closed .avatar {
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ }
+
+ .name {
+ white-space: nowrap;
+ font-weight: bold;
+ }
+ .closed .name {
+ width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-weight: normal;
+ }
+ </ui:style>
+
+ <g:HTMLPanel
+ styleName='{res.style.commentBox}'
+ addStyleNames='{style.closed}'>
+ <c:AvatarImage ui:field='avatar' styleName='{style.avatar}'/>
+ <div class='{res.style.contents}'>
+ <g:HTMLPanel ui:field='header' styleName='{res.style.header}'>
+ <div ui:field='name' class='{style.name}'/>
+ <div ui:field='summary' class='{res.style.summary}'/>
+ <div ui:field='date' class='{res.style.date}'/>
+ </g:HTMLPanel>
+ <div ui:field='message' aria-hidden='true' style='display: NONE'/>
+ <div ui:field='buttons' aria-hidden='true' style='display: NONE'>
+ <g:Button ui:field='reply' styleName=''
+ title='Reply to this comment'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Reply</ui:msg></div>
+ </g:Button>
+ <g:Button ui:field='done' styleName=''
+ title='Reply "Done" to this comment'>
+ <ui:attribute name='title'/>
+ <div><ui:msg>Done</ui:msg></div>
+ </g:Button>
+ </div>
+ </div>
+ </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
new file mode 100644
index 0000000..b7840c2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.CssResource;
+
+/** Resources used by diff. */
+interface Resources extends ClientBundle {
+ static final Resources I = GWT.create(Resources.class);
+
+ @Source("CommentBoxUi.css") Style style();
+
+ interface Style extends CssResource {
+ String commentBox();
+ String contents();
+ String header();
+ String summary();
+ String date();
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
new file mode 100644
index 0000000..951534b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
@@ -0,0 +1,1369 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.change.ChangeScreen2;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
+import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
+import com.google.gerrit.client.changes.ChangeList;
+import com.google.gerrit.client.changes.CommentApi;
+import com.google.gerrit.client.changes.CommentInfo;
+import com.google.gerrit.client.diff.DiffInfo.Region;
+import com.google.gerrit.client.diff.DiffInfo.Span;
+import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
+import com.google.gerrit.client.diff.PaddingManager.LinePaddingWidgetWrapper;
+import com.google.gerrit.client.diff.PaddingManager.PaddingWidgetWrapper;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.patches.SkippedLine;
+import com.google.gerrit.client.projects.ConfigInfoCache;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.common.changes.Side;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+import com.google.gwtexpui.globalkey.client.ShowHelpCommand;
+import com.google.gwtexpui.user.client.DialogVisibleEvent;
+import com.google.gwtexpui.user.client.DialogVisibleHandler;
+import com.google.gwtexpui.user.client.UserAgent;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.CodeMirror.EventHandler;
+import net.codemirror.lib.CodeMirror.GutterClickHandler;
+import net.codemirror.lib.CodeMirror.LineClassWhere;
+import net.codemirror.lib.CodeMirror.LineHandle;
+import net.codemirror.lib.CodeMirror.RenderLineHandler;
+import net.codemirror.lib.CodeMirror.Viewport;
+import net.codemirror.lib.Configuration;
+import net.codemirror.lib.KeyMap;
+import net.codemirror.lib.LineCharacter;
+import net.codemirror.lib.LineWidget;
+import net.codemirror.lib.ModeInjector;
+import net.codemirror.lib.ScrollInfo;
+import net.codemirror.lib.TextMarker.FromTo;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class SideBySide2 extends Screen {
+ interface Binder extends UiBinder<FlowPanel, SideBySide2> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ private static final JsArrayString EMPTY =
+ JavaScriptObject.createArray().cast();
+
+ @UiField(provided = true)
+ Header header;
+
+ @UiField(provided = true)
+ DiffTable diffTable;
+
+ private final Change.Id changeId;
+ private final PatchSet.Id base;
+ private final PatchSet.Id revision;
+ private final String path;
+ private AccountDiffPreference pref;
+
+ private CodeMirror cmA;
+ private CodeMirror cmB;
+ private CodeMirror lastFocused;
+ private Timer scrollTimerA;
+ private Timer scrollTimerB;
+ private HandlerRegistration resizeHandler;
+ private JsArray<CommentInfo> publishedBase;
+ private JsArray<CommentInfo> publishedRevision;
+ private JsArray<CommentInfo> draftsBase;
+ private JsArray<CommentInfo> draftsRevision;
+ private DiffInfo diff;
+ private LineMapper mapper;
+ private CommentLinkProcessor commentLinkProcessor;
+ private Map<String, PublishedBox> publishedMap;
+ private Map<LineHandle, CommentBox> lineActiveBoxMap;
+ private Map<LineHandle, List<PublishedBox>> linePublishedBoxesMap;
+ private Map<LineHandle, PaddingManager> linePaddingManagerMap;
+ private Map<LineHandle, LinePaddingWidgetWrapper> linePaddingOnOtherSideMap;
+ private List<DiffChunkInfo> diffChunks;
+ private List<SkippedLine> skips;
+ private int context;
+
+ private KeyCommandSet keysNavigation;
+ private KeyCommandSet keysAction;
+ private KeyCommandSet keysComment;
+ private KeyCommandSet keysOpenByEnter;
+ private List<HandlerRegistration> handlers;
+
+ public SideBySide2(
+ PatchSet.Id base,
+ PatchSet.Id revision,
+ String path) {
+ this.base = base;
+ this.revision = revision;
+ this.changeId = revision.getParentKey();
+ this.path = path;
+
+ pref = Gerrit.getAccountDiffPreference();
+ if (pref == null) {
+ pref = AccountDiffPreference.createDefault(null);
+ }
+ context = pref.getContext();
+
+ handlers = new ArrayList<HandlerRegistration>(6);
+ // TODO: Re-implement necessary GlobalKey bindings.
+ addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
+ keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
+ add(header = new Header(keysNavigation, revision, path));
+ add(diffTable = new DiffTable(this, base, revision, path));
+ add(uiBinder.createAndBindUi(this));
+ }
+
+ @Override
+ protected void onInitUI() {
+ super.onInitUI();
+ setHeaderVisible(false);
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+
+ CallbackGroup cmGroup = new CallbackGroup();
+ CodeMirror.initLibrary(cmGroup.add(CallbackGroup.<Void> emptyCallback()));
+ final CallbackGroup group = new CallbackGroup();
+ final AsyncCallback<Void> modeInjectorCb =
+ group.add(CallbackGroup.<Void> emptyCallback());
+
+ DiffApi.diff(revision, path)
+ .base(base)
+ .wholeFile()
+ .intraline(pref.isIntralineDifference())
+ .ignoreWhitespace(pref.getIgnoreWhitespace())
+ .get(cmGroup.addFinal(new GerritCallback<DiffInfo>() {
+ @Override
+ public void onSuccess(DiffInfo diffInfo) {
+ diff = diffInfo;
+ new ModeInjector()
+ .add(getContentType(diff.meta_a()))
+ .add(getContentType(diff.meta_b()))
+ .inject(modeInjectorCb);
+ }
+ }));
+
+ if (base != null) {
+ CommentApi.comments(base, group.add(getCommentCallback(DisplaySide.A, false)));
+ }
+ CommentApi.comments(revision, group.add(getCommentCallback(DisplaySide.B, false)));
+
+ if (Gerrit.isSignedIn()) {
+ if (base != null) {
+ CommentApi.drafts(base, group.add(getCommentCallback(DisplaySide.A, true)));
+ }
+ CommentApi.drafts(revision, group.add(getCommentCallback(DisplaySide.B, true)));
+ }
+
+ ConfigInfoCache.get(changeId, group.addFinal(
+ new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide2.this) {
+ @Override
+ protected void preDisplay(ConfigInfoCache.Entry result) {
+ commentLinkProcessor = result.getCommentLinkProcessor();
+ setTheme(result.getTheme());
+
+ DiffInfo diffInfo = diff;
+ diff = null;
+ display(diffInfo);
+ }
+ }));
+
+ RestApi call = ChangeApi.detail(changeId.get());
+ ChangeList.addOptions(call, EnumSet.of(
+ ListChangesOption.ALL_REVISIONS));
+ call.get(new GerritCallback<ChangeInfo>() {
+ @Override
+ public void onSuccess(ChangeInfo info) {
+ info.revisions().copyKeysIntoChildren("name");
+ JsArray<RevisionInfo> list = info.revisions().values();
+ RevisionInfo.sortRevisionInfoByNumber(list);
+ diffTable.setUpPatchSetNav(list);
+ }});
+ }
+
+ @Override
+ public void onShowView() {
+ super.onShowView();
+
+ handlers.add(UserAgent.addDialogVisibleHandler(new DialogVisibleHandler() {
+ @Override
+ public void onDialogVisible(DialogVisibleEvent event) {
+ diffTable.getElement().getStyle().setVisibility(
+ event.isVisible()
+ ? Style.Visibility.HIDDEN
+ : Style.Visibility.VISIBLE);
+ }
+ }));
+ resizeCodeMirror();
+
+ Window.enableScrolling(false);
+ cmA.setOption("viewportMargin", 10);
+ cmB.setOption("viewportMargin", 10);
+ cmB.setCursor(LineCharacter.create(0));
+ cmB.focus();
+
+ prefetchNextFile();
+ }
+
+ @Override
+ protected void onUnload() {
+ super.onUnload();
+
+ removeKeyHandlerRegs();
+ if (resizeHandler != null) {
+ resizeHandler.removeHandler();
+ resizeHandler = null;
+ }
+ cmA.getWrapperElement().removeFromParent();
+ cmB.getWrapperElement().removeFromParent();
+ Window.enableScrolling(true);
+ Gerrit.setHeaderVisible(true);
+ }
+
+ private void removeKeyHandlerRegs() {
+ for (HandlerRegistration h : handlers) {
+ h.removeHandler();
+ }
+ handlers.clear();
+ }
+
+ private void registerCmEvents(final CodeMirror cm) {
+ cm.on("cursorActivity", updateActiveLine(cm));
+ cm.on("gutterClick", onGutterClick(cm));
+ cm.on("scroll", doScroll(cm));
+ scrollTimerA = new Timer() {
+ @Override
+ public void run() {
+ fixScroll(cmA);
+ }
+ };
+ scrollTimerB = new Timer() {
+ @Override
+ public void run() {
+ fixScroll(cmB);
+ }
+ };
+ cm.on("renderLine", resizeLinePadding(getSideFromCm(cm)));
+ cm.on("viewportChange", adjustGutters(cm));
+ cm.on("focus", new Runnable() {
+ @Override
+ public void run() {
+ lastFocused = cm;
+ updateActiveLine(cm).run();
+ }
+ });
+ cm.on("contextmenu", new EventHandler() {
+ @Override
+ public void handle(CodeMirror instance, NativeEvent event) {
+ CodeMirror.setObjectProperty(event, "codemirrorIgnore", true);
+ lastFocused.focus();
+ }
+ });
+ cm.addKeyMap(KeyMap.create()
+ .on("'a'", upToChange(true))
+ .on("'u'", upToChange(false))
+ .on("'r'", toggleReviewed())
+ .on("'o'", toggleOpenBox(cm))
+ .on("Enter", toggleOpenBox(cm))
+ .on("'c'", insertNewDraft(cm))
+ .on("Alt-U", new Runnable() {
+ public void run() {
+ cm.getInputField().blur();
+ clearActiveLine(cm);
+ clearActiveLine(otherCm(cm));
+ }
+ })
+ .on("[", new Runnable() {
+ @Override
+ public void run() {
+ (header.hasPrev() ? header.prev : header.up).go();
+ }
+ })
+ .on("]", new Runnable() {
+ @Override
+ public void run() {
+ (header.hasNext() ? header.next : header.up).go();
+ }
+ })
+ .on("Shift-Alt-/", new Runnable() {
+ @Override
+ public void run() {
+ new ShowHelpCommand().onKeyPress(null);
+ }
+ })
+ .on("N", maybeNextVimSearch(cm))
+ .on("P", diffChunkNav(cm, true))
+ .on("Shift-O", openClosePublished(cm))
+ .on("Shift-Left", flipCursorSide(cm, true))
+ .on("Shift-Right", flipCursorSide(cm, false)));
+ }
+
+ @Override
+ public void registerKeys() {
+ super.registerKeys();
+
+ keysNavigation.add(new UpToChangeCommand2(revision, 0, 'u'));
+ keysNavigation.add(new NoOpKeyCommand(0, 'j', PatchUtil.C.lineNext()));
+ keysNavigation.add(new NoOpKeyCommand(0, 'k', PatchUtil.C.linePrev()));
+
+ keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
+ keysAction.add(new NoOpKeyCommand(0, 'o', PatchUtil.C.expandComment()));
+ keysAction.add(new KeyCommand(0, 'r', PatchUtil.C.toggleReviewed()) {
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ toggleReviewed().run();
+ }
+ });
+ keysAction.add(new KeyCommand(0, 'a', PatchUtil.C.openReply()) {
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ upToChange(true).run();
+ }
+ });
+
+ keysOpenByEnter = new KeyCommandSet(Gerrit.C.sectionNavigation());
+ keysOpenByEnter.add(new NoOpKeyCommand(0, KeyCodes.KEY_ENTER,
+ PatchUtil.C.expandComment()));
+
+ if (Gerrit.isSignedIn()) {
+ keysAction.add(new NoOpKeyCommand(0, 'c', PatchUtil.C.commentInsert()));
+ keysComment = new KeyCommandSet(PatchUtil.C.commentEditorSet());
+ keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 's',
+ PatchUtil.C.commentSaveDraft()));
+ keysComment.add(new NoOpKeyCommand(0, KeyCodes.KEY_ESCAPE,
+ PatchUtil.C.commentCancelEdit()));
+ } else {
+ keysComment = null;
+ }
+ removeKeyHandlerRegs();
+ handlers.add(GlobalKey.add(this, keysNavigation));
+ handlers.add(GlobalKey.add(this, keysAction));
+ handlers.add(GlobalKey.add(this, keysOpenByEnter));
+ if (keysComment != null) {
+ handlers.add(GlobalKey.add(this, keysComment));
+ }
+ }
+
+ private GerritCallback<NativeMap<JsArray<CommentInfo>>> getCommentCallback(
+ final DisplaySide side, final boolean toDrafts) {
+ return new GerritCallback<NativeMap<JsArray<CommentInfo>>>() {
+ @Override
+ public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+ JsArray<CommentInfo> in = result.get(path);
+ if (in != null) {
+ if (toDrafts) {
+ if (side == DisplaySide.A) {
+ draftsBase = in;
+ } else {
+ draftsRevision = in;
+ }
+ } else {
+ if (side == DisplaySide.A) {
+ publishedBase = in;
+ } else {
+ publishedRevision = in;
+ }
+ }
+ }
+ }
+ };
+ }
+
+ private void display(DiffInfo diffInfo) {
+ cmA = displaySide(diffInfo.meta_a(), diffInfo.text_a(), diffTable.cmA);
+ cmB = displaySide(diffInfo.meta_b(), diffInfo.text_b(), diffTable.cmB);
+ skips = new ArrayList<SkippedLine>();
+ linePaddingOnOtherSideMap = new HashMap<LineHandle, LinePaddingWidgetWrapper>();
+ diffChunks = new ArrayList<DiffChunkInfo>();
+ render(diffInfo);
+ lineActiveBoxMap = new HashMap<LineHandle, CommentBox>();
+ linePublishedBoxesMap = new HashMap<LineHandle, List<PublishedBox>>();
+ linePaddingManagerMap = new HashMap<LineHandle, PaddingManager>();
+ if (publishedBase != null || publishedRevision != null) {
+ publishedMap = new HashMap<String, PublishedBox>();
+ }
+ if (publishedBase != null) {
+ renderPublished(publishedBase);
+ }
+ if (publishedRevision != null) {
+ renderPublished(publishedRevision);
+ }
+ if (draftsBase != null) {
+ renderDrafts(draftsBase);
+ }
+ if (draftsRevision != null) {
+ renderDrafts(draftsRevision);
+ }
+ renderSkips();
+ registerCmEvents(cmA);
+ registerCmEvents(cmB);
+ resizeHandler = Window.addResizeHandler(new ResizeHandler() {
+ @Override
+ public void onResize(ResizeEvent event) {
+ resizeCodeMirror();
+ }
+ });
+ if (pref.isShowTabs()) {
+ diffTable.addStyleName(DiffTable.style.showtabs());
+ }
+ }
+
+ private CodeMirror displaySide(DiffInfo.FileMeta meta, String contents,
+ Element ele) {
+ if (meta == null) {
+ contents = "";
+ }
+ Configuration cfg = Configuration.create()
+ .set("readOnly", true)
+ .set("lineNumbers", true)
+ .set("tabSize", pref.getTabSize())
+ .set("mode", getContentType(meta))
+ .set("lineWrapping", true)
+ .set("styleSelectedText", true)
+ .set("showTrailingSpace", pref.isShowWhitespaceErrors())
+ .set("keyMap", "vim_ro")
+ .set("value", contents)
+ /**
+ * Without this, CM won't put line widgets too far down in the right spot,
+ * and padding widgets will be informed of wrong offset height. Reset to
+ * 10 (default) after initial rendering.
+ */
+ .setInfinity("viewportMargin");
+ int h = Gerrit.getHeaderFooterHeight() + 18 /* reviewed estimate */;
+ CodeMirror cm = CodeMirror.create(ele, cfg);
+ cm.setHeight(Window.getClientHeight() - h);
+ return cm;
+ }
+
+ private void render(DiffInfo diff) {
+ JsArray<Region> regions = diff.content();
+ String diffColor = diff.meta_a() == null || diff.meta_b() == null
+ ? DiffTable.style.intralineBg()
+ : DiffTable.style.diff();
+ mapper = new LineMapper();
+ for (int i = 0; i < regions.length(); i++) {
+ Region current = regions.get(i);
+ int origLineA = mapper.getLineA();
+ int origLineB = mapper.getLineB();
+ if (current.ab() != null) { // Common
+ int length = current.ab().length();
+ mapper.appendCommon(length);
+ if (i == 0 && length > context + 1) {
+ skips.add(new SkippedLine(0, 0, length - context));
+ } else if (i == regions.length() - 1 && length > context + 1) {
+ skips.add(new SkippedLine(origLineA + context, origLineB + context,
+ length - context));
+ } else if (length > 2 * context + 1) {
+ skips.add(new SkippedLine(origLineA + context, origLineB + context,
+ length - 2 * context));
+ }
+ } else { // Insert, Delete or Edit
+ JsArrayString currentA = current.a() == null ? EMPTY : current.a();
+ JsArrayString currentB = current.b() == null ? EMPTY : current.b();
+ int aLength = currentA.length();
+ int bLength = currentB.length();
+ String color = currentA == EMPTY || currentB == EMPTY
+ ? diffColor
+ : DiffTable.style.intralineBg();
+ colorLines(cmA, color, origLineA, aLength);
+ colorLines(cmB, color, origLineB, bLength);
+ int commonCnt = Math.min(aLength, bLength);
+ mapper.appendCommon(commonCnt);
+ if (aLength < bLength) { // Edit with insertion
+ int insertCnt = bLength - aLength;
+ mapper.appendInsert(insertCnt);
+ } else if (aLength > bLength) { // Edit with deletion
+ int deleteCnt = aLength - bLength;
+ mapper.appendDelete(deleteCnt);
+ }
+ int chunkEndA = mapper.getLineA() - 1;
+ int chunkEndB = mapper.getLineB() - 1;
+ if (aLength > 0) {
+ addDiffChunkAndPadding(cmB, chunkEndB, chunkEndA, aLength, bLength > 0);
+ }
+ if (bLength > 0) {
+ addDiffChunkAndPadding(cmA, chunkEndA, chunkEndB, bLength, aLength > 0);
+ }
+ markEdit(cmA, currentA, current.edit_a(), origLineA);
+ markEdit(cmB, currentB, current.edit_b(), origLineB);
+ if (aLength == 0) {
+ diffTable.sidePanel.addGutter(cmB, origLineB, SidePanel.GutterType.INSERT);
+ } else if (bLength == 0) {
+ diffTable.sidePanel.addGutter(cmA, origLineA, SidePanel.GutterType.DELETE);
+ } else {
+ diffTable.sidePanel.addGutter(cmB, origLineB, SidePanel.GutterType.EDIT);
+ }
+ }
+ }
+ }
+
+ private DraftBox addNewDraft(CodeMirror cm, int line, FromTo fromTo) {
+ DisplaySide side = getSideFromCm(cm);
+ return addDraftBox(CommentInfo.createRange(
+ path,
+ getStoredSideFromDisplaySide(side),
+ line + 1,
+ null,
+ null,
+ CommentRange.create(fromTo)), side);
+ }
+
+ CommentInfo createReply(CommentInfo replyTo) {
+ if (!replyTo.has_line() && replyTo.range() == null) {
+ return CommentInfo.createFile(path, replyTo.side(), replyTo.id(), null);
+ } else {
+ return CommentInfo.createRange(path, replyTo.side(), replyTo.line(),
+ replyTo.id(), null, replyTo.range());
+ }
+ }
+
+ DraftBox addDraftBox(CommentInfo info, DisplaySide side) {
+ CodeMirror cm = getCmFromSide(side);
+ final DraftBox box = new DraftBox(this, cm, side, commentLinkProcessor,
+ getPatchSetIdFromSide(side), info);
+ if (info.id() == null) {
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ box.setOpen(true);
+ box.setEdit(true);
+ }
+ });
+ }
+ if (!info.has_line()) {
+ return box;
+ }
+ addCommentBox(info, box);
+ LineHandle handle = cm.getLineHandle(info.line() - 1);
+ lineActiveBoxMap.put(handle, box);
+ return box;
+ }
+
+ CommentBox addCommentBox(CommentInfo info, CommentBox box) {
+ diffTable.add(box);
+ DisplaySide side = box.getSide();
+ CodeMirror cm = getCmFromSide(side);
+ CodeMirror other = otherCm(cm);
+ int line = info.line() - 1; // CommentInfo is 1-based, but CM is 0-based
+ LineHandle handle = cm.getLineHandle(line);
+ PaddingManager manager;
+ if (linePaddingManagerMap.containsKey(handle)) {
+ manager = linePaddingManagerMap.get(handle);
+ } else {
+ // Estimated height at 28px, fixed by deferring after display
+ manager = new PaddingManager(
+ addPaddingWidget(cm, DiffTable.style.padding(), line, 0, Unit.PX, 0));
+ linePaddingManagerMap.put(handle, manager);
+ }
+ int lineToPad = mapper.lineOnOther(side, line).getLine();
+ LineHandle otherHandle = other.getLineHandle(lineToPad);
+ DiffChunkInfo myChunk = getDiffChunk(side, line);
+ DiffChunkInfo otherChunk = getDiffChunk(getSideFromCm(other), lineToPad);
+ PaddingManager otherManager;
+ if (linePaddingManagerMap.containsKey(otherHandle)) {
+ otherManager = linePaddingManagerMap.get(otherHandle);
+ } else {
+ otherManager = new PaddingManager(
+ addPaddingWidget(other, DiffTable.style.padding(), lineToPad, 0, Unit.PX, 0));
+ linePaddingManagerMap.put(otherHandle, otherManager);
+ }
+ if ((myChunk == null && otherChunk == null) || (myChunk != null && otherChunk != null)) {
+ PaddingManager.link(manager, otherManager);
+ }
+ int index = manager.getCurrentCount();
+ manager.insert(box, index);
+ Configuration config = Configuration.create()
+ .set("coverGutter", true)
+ .set("insertAt", index);
+ LineWidget boxWidget = cm.addLineWidget(line, box.getElement(), config);
+ box.setPaddingManager(manager);
+ box.setSelfWidgetWrapper(new PaddingWidgetWrapper(boxWidget, box.getElement()));
+ box.setParent(this);
+ if (otherChunk == null) {
+ box.setDiffChunkInfo(myChunk);
+ }
+ box.setGutterWrapper(diffTable.sidePanel.addGutter(cm, info.line() - 1,
+ box instanceof DraftBox ?
+ SidePanel.GutterType.DRAFT
+ : SidePanel.GutterType.COMMENT));
+ return box;
+ }
+
+ void removeDraft(DraftBox box, int line) {
+ LineHandle handle = getCmFromSide(box.getSide()).getLineHandle(line);
+ lineActiveBoxMap.remove(handle);
+ if (linePublishedBoxesMap.containsKey(handle)) {
+ List<PublishedBox> list = linePublishedBoxesMap.get(handle);
+ lineActiveBoxMap.put(handle, list.get(list.size() - 1));
+ }
+ }
+
+ void addFileCommentBox(CommentBox box) {
+ diffTable.addFileCommentBox(box);
+ }
+
+ void removeFileCommentBox(DraftBox box) {
+ diffTable.onRemoveDraftBox(box);
+ }
+
+ private List<CommentInfo> sortComment(JsArray<CommentInfo> unsorted) {
+ List<CommentInfo> sorted = new ArrayList<CommentInfo>();
+ for (int i = 0; i < unsorted.length(); i++) {
+ sorted.add(unsorted.get(i));
+ }
+ Collections.sort(sorted, new Comparator<CommentInfo>() {
+ @Override
+ public int compare(CommentInfo o1, CommentInfo o2) {
+ return o1.updated().compareTo(o2.updated());
+ }
+ });
+ return sorted;
+ }
+
+ private void renderPublished(JsArray<CommentInfo> published) {
+ List<CommentInfo> sorted = sortComment(published);
+ for (CommentInfo info : sorted) {
+ DisplaySide side;
+ if (info.side() == Side.PARENT) {
+ if (base != null) {
+ continue;
+ }
+ side = DisplaySide.A;
+ } else {
+ side = published == publishedBase ? DisplaySide.A : DisplaySide.B;
+ }
+ CodeMirror cm = getCmFromSide(side);
+ PublishedBox box = new PublishedBox(this, cm, side, commentLinkProcessor,
+ getPatchSetIdFromSide(side), info);
+ publishedMap.put(info.id(), box);
+ if (!info.has_line()) {
+ diffTable.addFileCommentBox(box);
+ continue;
+ }
+ int line = info.line() - 1;
+ LineHandle handle = cm.getLineHandle(line);
+ if (linePublishedBoxesMap.containsKey(handle)) {
+ linePublishedBoxesMap.get(handle).add(box);
+ } else {
+ List<PublishedBox> list = new ArrayList<PublishedBox>();
+ list.add(box);
+ linePublishedBoxesMap.put(handle, list);
+ }
+ lineActiveBoxMap.put(handle, box);
+ addCommentBox(info, box);
+ }
+ }
+
+ private void renderDrafts(JsArray<CommentInfo> drafts) {
+ List<CommentInfo> sorted = sortComment(drafts);
+ for (CommentInfo info : sorted) {
+ DisplaySide side;
+ if (info.side() == Side.PARENT) {
+ if (base != null) {
+ continue;
+ }
+ side = DisplaySide.A;
+ } else {
+ side = drafts == draftsBase ? DisplaySide.A : DisplaySide.B;
+ }
+ DraftBox box = new DraftBox(
+ this, getCmFromSide(side), side, commentLinkProcessor,
+ getPatchSetIdFromSide(side), info);
+ if (publishedBase != null || publishedRevision != null) {
+ PublishedBox replyToBox = publishedMap.get(info.in_reply_to());
+ if (replyToBox != null) {
+ replyToBox.registerReplyBox(box);
+ }
+ }
+ if (!info.has_line()) {
+ diffTable.addFileCommentBox(box);
+ continue;
+ }
+ lineActiveBoxMap.put(
+ getCmFromSide(side).getLineHandle(info.line() - 1), box);
+ addCommentBox(info, box);
+ }
+ }
+
+ private void renderSkips() {
+ if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+ return;
+ }
+
+ /**
+ * TODO: This is not optimal, but shouldn't bee too costly in most cases.
+ * Maybe rewrite after done keeping track of diff chunk positions.
+ */
+ for (CommentBox box : lineActiveBoxMap.values()) {
+ List<SkippedLine> temp = new ArrayList<SkippedLine>();
+ for (SkippedLine skip : skips) {
+ CommentInfo info = box.getCommentInfo();
+ int startLine = box.getSide() == DisplaySide.A
+ ? skip.getStartA()
+ : skip.getStartB();
+ int boxLine = info.line();
+ int deltaBefore = boxLine - startLine;
+ int deltaAfter = startLine + skip.getSize() - boxLine;
+ if (deltaBefore < -context || deltaAfter < -context) {
+ temp.add(skip); // Size guaranteed to be greater than 1
+ } else if (deltaBefore > context && deltaAfter > context) {
+ SkippedLine before = new SkippedLine(
+ skip.getStartA(), skip.getStartB(),
+ skip.getSize() - deltaAfter - context);
+ skip.incrementStart(deltaBefore + context);
+ checkAndAddSkip(temp, before);
+ checkAndAddSkip(temp, skip);
+ } else if (deltaAfter > context) {
+ skip.incrementStart(deltaBefore + context);
+ checkAndAddSkip(temp, skip);
+ } else if (deltaBefore > context) {
+ skip.reduceSize(deltaAfter + context);
+ checkAndAddSkip(temp, skip);
+ }
+ }
+ if (temp.isEmpty()) {
+ return;
+ }
+ skips = temp;
+ }
+ for (SkippedLine skip : skips) {
+ SkipBar barA = renderSkipHelper(cmA, skip);
+ SkipBar barB = renderSkipHelper(cmB, skip);
+ SkipBar.link(barA, barB);
+ }
+ }
+
+ private void checkAndAddSkip(List<SkippedLine> list, SkippedLine toAdd) {
+ if (toAdd.getSize() > 1) {
+ list.add(toAdd);
+ }
+ }
+
+ private SkipBar renderSkipHelper(CodeMirror cm, SkippedLine skip) {
+ int size = skip.getSize();
+ int markStart = cm == cmA ? skip.getStartA() - 1 : skip.getStartB() - 1;
+ int markEnd = markStart + size;
+ SkipBar bar = new SkipBar(cm);
+ diffTable.add(bar);
+ /**
+ * Due to CodeMirror limitation, there's no way to make the first
+ * line disappear completely, and CodeMirror doesn't like manually
+ * setting the display of a line to "none". The workaround here uses
+ * inline widget for the first line and regular line widgets for others.
+ */
+ Configuration markerConfig;
+ if (markStart == -1) {
+ markerConfig = Configuration.create()
+ .set("inclusiveLeft", true)
+ .set("inclusiveRight", true)
+ .set("replacedWith", bar.getElement());
+ cm.addLineClass(0, LineClassWhere.WRAP, DiffTable.style.hideNumber());
+ } else {
+ markerConfig = Configuration.create().set("collapsed", true);
+ Configuration config = Configuration.create().set("coverGutter", true);
+ bar.setWidget(cm.addLineWidget(markStart, bar.getElement(), config));
+ }
+ bar.setMarker(cm.markText(CodeMirror.pos(markStart),
+ CodeMirror.pos(markEnd), markerConfig), size);
+ return bar;
+ }
+
+ private CodeMirror otherCm(CodeMirror me) {
+ return me == cmA ? cmB : cmA;
+ }
+
+ private PatchSet.Id getPatchSetIdFromSide(DisplaySide side) {
+ return side == DisplaySide.A && base != null ? base : revision;
+ }
+
+ private CodeMirror getCmFromSide(DisplaySide side) {
+ return side == DisplaySide.A ? cmA : cmB;
+ }
+
+ private DisplaySide getSideFromCm(CodeMirror cm) {
+ return cm == cmA ? DisplaySide.A : DisplaySide.B;
+ }
+
+ Side getStoredSideFromDisplaySide(DisplaySide side) {
+ return side == DisplaySide.A && base == null ? Side.PARENT : Side.REVISION;
+ }
+
+ private void markEdit(CodeMirror cm, JsArrayString lines,
+ JsArray<Span> edits, int startLine) {
+ if (edits == null) {
+ return;
+ }
+ EditIterator iter = new EditIterator(lines, startLine);
+ Configuration intralineBgOpt = Configuration.create()
+ .set("className", DiffTable.style.intralineBg())
+ .set("readOnly", true);
+ Configuration diffOpt = Configuration.create()
+ .set("className", DiffTable.style.diff())
+ .set("readOnly", true);
+ LineCharacter last = CodeMirror.pos(0, 0);
+ for (int i = 0; i < edits.length(); i++) {
+ Span span = edits.get(i);
+ LineCharacter from = iter.advance(span.skip());
+ LineCharacter to = iter.advance(span.mark());
+ int fromLine = from.getLine();
+ if (last.getLine() == fromLine) {
+ cm.markText(last, from, intralineBgOpt);
+ } else {
+ cm.markText(CodeMirror.pos(fromLine, 0), from, intralineBgOpt);
+ }
+ cm.markText(from, to, diffOpt);
+ last = to;
+ for (int line = fromLine; line < to.getLine(); line++) {
+ cm.addLineClass(line, LineClassWhere.BACKGROUND,
+ DiffTable.style.diff());
+ }
+ }
+ }
+
+ private void colorLines(CodeMirror cm, String color, int line, int cnt) {
+ for (int i = 0; i < cnt; i++) {
+ cm.addLineClass(line + i, LineClassWhere.WRAP, color);
+ }
+ }
+
+ private void addDiffChunkAndPadding(CodeMirror cmToPad, int lineToPad,
+ int lineOnOther, int chunkSize, boolean edit) {
+ CodeMirror otherCm = otherCm(cmToPad);
+ linePaddingOnOtherSideMap.put(otherCm.getLineHandle(lineOnOther),
+ new LinePaddingWidgetWrapper(addPaddingWidget(cmToPad, DiffTable.style.padding(),
+ lineToPad, 0, Unit.EM, null), lineToPad, chunkSize));
+ diffChunks.add(new DiffChunkInfo(getSideFromCm(otherCm),
+ lineOnOther - chunkSize + 1, lineOnOther, edit));
+ }
+
+ private PaddingWidgetWrapper addPaddingWidget(CodeMirror cm, String style,
+ int line, double height, Unit unit, Integer index) {
+ Element div = DOM.createDiv();
+ div.setClassName(style);
+ div.getStyle().setHeight(height, unit);
+ Configuration config = Configuration.create()
+ .set("coverGutter", true)
+ .set("above", line == -1);
+ if (index != null) {
+ config = config.set("insertAt", index);
+ }
+ LineWidget widget = cm.addLineWidget(line == -1 ? 0 : line, div, config);
+ return new PaddingWidgetWrapper(widget, div);
+ }
+
+ private void clearActiveLine(CodeMirror cm) {
+ if (cm.hasActiveLine()) {
+ LineHandle activeLine = cm.getActiveLine();
+ cm.removeLineClass(activeLine,
+ LineClassWhere.WRAP, DiffTable.style.activeLine());
+ cm.removeLineClass(activeLine,
+ LineClassWhere.BACKGROUND, DiffTable.style.activeLineBg());
+ cm.setActiveLine(null);
+ }
+ }
+
+ private Runnable doScroll(final CodeMirror cm) {
+ return new Runnable() {
+ public void run() {
+ // Hack to prevent feedback loop.
+ if (cm.getScrollSetAt() + 5 > System.currentTimeMillis()) {
+ return;
+ }
+ if (cm == cmA) {
+ scrollTimerB.cancel();
+ scrollTimerA.schedule(5);
+ } else {
+ scrollTimerA.cancel();
+ scrollTimerB.schedule(5);
+ }
+ }
+ };
+ }
+
+ private void fixScroll(CodeMirror cm) {
+ ScrollInfo si = cm.getScrollInfo();
+ if (si.getTop() == 0 && !Gerrit.isHeaderVisible()) {
+ Gerrit.setHeaderVisible(true);
+ diffTable.updateFileCommentVisibility(false);
+ } else if (si.getTop() > 0.5 * si.getClientHeight()
+ && Gerrit.isHeaderVisible()) {
+ Gerrit.setHeaderVisible(false);
+ diffTable.updateFileCommentVisibility(true);
+ }
+ CodeMirror other = otherCm(cm);
+ Viewport fromTo = cm.getViewport();
+ int line = fromTo.getFrom();
+ for (; line <= fromTo.getTo(); line++) {
+ LineOnOtherInfo info = mapper.lineOnOther(getSideFromCm(cm), line);
+ /**
+ * Since CM doesn't always take the height of line widgets into
+ * account when calculating scrollInfo when scrolling too fast
+ * (e.g. throw-scrolling), simply setting scrollTop to be the same
+ * doesn't guarantee alignment.
+ *
+ * Iterate over the viewport to find the first line that isn't part of an
+ * insertion or deletion gap, for which isAligned() will be true. We then
+ * manually examine if the lines that should be aligned are at the same
+ * height. If not, perform additional scrolling.
+ */
+ if (info.isAligned()) {
+ double myHeight = cm.heightAtLine(line);
+ double otherHeight = other.heightAtLine(info.getLine());
+ if (myHeight != otherHeight) {
+ other.scrollToY(
+ other.getScrollInfo().getTop() + otherHeight - myHeight);
+ other.setScrollSetAt(System.currentTimeMillis());
+ }
+ break;
+ }
+ }
+ }
+
+ private Runnable adjustGutters(final CodeMirror cm) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ Viewport fromTo = cm.getViewport();
+ int size = fromTo.getTo() - fromTo.getFrom() + 1;
+ if (cm.getOldViewportSize() == size) {
+ return;
+ }
+ cm.setOldViewportSize(size);
+ diffTable.sidePanel.adjustGutters(cmB);
+ }
+ };
+ }
+
+ private Runnable updateActiveLine(final CodeMirror cm) {
+ final CodeMirror other = otherCm(cm);
+ return new Runnable() {
+ public void run() {
+ /**
+ * The rendering of active lines has to be deferred. Reflow
+ * caused by adding and removing styles chokes Firefox when arrow
+ * key (or j/k) is held down. Performance on Chrome is fine
+ * without the deferral.
+ */
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ LineHandle handle = cm.getLineHandleVisualStart(
+ cm.getCursor("end").getLine());
+ if (cm.hasActiveLine() && cm.getActiveLine().equals(handle)) {
+ return;
+ }
+
+ clearActiveLine(cm);
+ clearActiveLine(other);
+ cm.setActiveLine(handle);
+ cm.addLineClass(
+ handle, LineClassWhere.WRAP, DiffTable.style.activeLine());
+ cm.addLineClass(
+ handle, LineClassWhere.BACKGROUND, DiffTable.style.activeLineBg());
+ LineOnOtherInfo info =
+ mapper.lineOnOther(getSideFromCm(cm), cm.getLineNumber(handle));
+ if (info.isAligned()) {
+ LineHandle oLineHandle = other.getLineHandle(info.getLine());
+ other.setActiveLine(oLineHandle);
+ other.addLineClass(oLineHandle, LineClassWhere.WRAP,
+ DiffTable.style.activeLine());
+ other.addLineClass(oLineHandle, LineClassWhere.BACKGROUND,
+ DiffTable.style.activeLineBg());
+ }
+ }
+ });
+ }
+ };
+ }
+
+ private GutterClickHandler onGutterClick(final CodeMirror cm) {
+ return new GutterClickHandler() {
+ @Override
+ public void handle(CodeMirror instance, int line, String gutter,
+ NativeEvent clickEvent) {
+ if (!(cm.hasActiveLine() &&
+ cm.getLineNumber(cm.getActiveLine()) == line)) {
+ cm.setCursor(LineCharacter.create(line));
+ }
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ insertNewDraft(cm).run();
+ }
+ });
+ }
+ };
+ }
+
+ private Runnable insertNewDraft(final CodeMirror cm) {
+ if (!Gerrit.isSignedIn()) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ Gerrit.doSignIn(getToken());
+ }
+ };
+ }
+ return new Runnable() {
+ public void run() {
+ LineHandle handle = cm.getActiveLine();
+ int line = cm.getLineNumber(handle);
+ CommentBox box = lineActiveBoxMap.get(handle);
+ FromTo fromTo = cm.getSelectedRange();
+ if (cm.somethingSelected()) {
+ lineActiveBoxMap.put(handle,
+ addNewDraft(cm, line, fromTo.getTo().getLine() == line ? fromTo : null));
+ } else if (box == null) {
+ lineActiveBoxMap.put(handle, addNewDraft(cm, line, null));
+ } else if (box instanceof DraftBox) {
+ ((DraftBox) box).setEdit(true);
+ } else {
+ ((PublishedBox) box).doReply();
+ }
+ }
+ };
+ }
+
+ private Runnable toggleOpenBox(final CodeMirror cm) {
+ return new Runnable() {
+ public void run() {
+ CommentBox box = lineActiveBoxMap.get(cm.getActiveLine());
+ if (box != null) {
+ box.setOpen(!box.isOpen());
+ }
+ }
+ };
+ }
+
+ private Runnable upToChange(final boolean openReplyBox) {
+ return new Runnable() {
+ public void run() {
+ String rev = String.valueOf(revision.get());
+ Gerrit.display(
+ PageLinks.toChange2(changeId, rev),
+ new ChangeScreen2(changeId, rev, openReplyBox));
+ }
+ };
+ }
+
+ private Runnable openClosePublished(final CodeMirror cm) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ if (cm.hasActiveLine()) {
+ List<PublishedBox> list =
+ linePublishedBoxesMap.get(cm.getActiveLine());
+ if (list == null) {
+ return;
+ }
+ boolean open = false;
+ for (PublishedBox box : list) {
+ if (!box.isOpen()) {
+ open = true;
+ break;
+ }
+ }
+ for (PublishedBox box : list) {
+ box.setOpen(open);
+ }
+ }
+ }
+ };
+ }
+
+ private Runnable toggleReviewed() {
+ return new Runnable() {
+ public void run() {
+ header.setReviewed(!header.isReviewed());
+ }
+ };
+ }
+
+ private Runnable flipCursorSide(final CodeMirror cm, final boolean toLeft) {
+ return new Runnable() {
+ public void run() {
+ if (cm.hasActiveLine() && (toLeft && cm == cmB || !toLeft && cm == cmA)) {
+ CodeMirror other = otherCm(cm);
+ other.setCursor(LineCharacter.create(
+ mapper.lineOnOther(
+ getSideFromCm(cm), cm.getLineNumber(cm.getActiveLine())).getLine()));
+ other.focus();
+ }
+ }
+ };
+ }
+
+ private Runnable maybeNextVimSearch(final CodeMirror cm) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ if (cm.hasVimSearchHighlight()) {
+ CodeMirror.handleVimKey(cm, "n");
+ } else {
+ diffChunkNav(cm, false).run();
+ }
+ }
+ };
+ }
+
+ private Runnable diffChunkNav(final CodeMirror cm, final boolean prev) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ int line = cm.hasActiveLine() ? cm.getLineNumber(cm.getActiveLine()) : 0;
+ int res = Collections.binarySearch(
+ diffChunks,
+ new DiffChunkInfo(getSideFromCm(cm), line, 0, false),
+ getDiffChunkComparator());
+ if (res < 0) {
+ res = -res - (prev ? 1 : 2);
+ }
+
+ res = res + (prev ? -1 : 1);
+ DiffChunkInfo lookUp = diffChunks.get(getWrapAroundDiffChunkIndex(res));
+ // If edit, skip the deletion chunk and set focus on the insertion one.
+ if (lookUp.isEdit() && lookUp.getSide() == DisplaySide.A) {
+ res = res + (prev ? -1 : 1);
+ }
+ DiffChunkInfo target = diffChunks.get(getWrapAroundDiffChunkIndex(res));
+ CodeMirror targetCm = getCmFromSide(target.getSide());
+ targetCm.setCursor(LineCharacter.create(target.getStart()));
+ targetCm.focus();
+ targetCm.scrollToY(Math.max(
+ 0,
+ targetCm.heightAtLine(target.getStart(), "local") -
+ 0.5 * cmB.getScrollbarV().getClientHeight()));
+ }
+ };
+ }
+
+ /**
+ * Diff chunks are ordered by their starting lines. If it's a deletion,
+ * use its corresponding line on the revision side for comparison. In
+ * the edit case, put the deletion chunk right before the insertion chunk.
+ * This placement guarantees well-ordering.
+ */
+ private Comparator<DiffChunkInfo> getDiffChunkComparator() {
+ return new Comparator<DiffChunkInfo>() {
+ @Override
+ public int compare(DiffChunkInfo o1, DiffChunkInfo o2) {
+ if (o1.getSide() == o2.getSide()) {
+ return o1.getStart() - o2.getStart();
+ } else if (o1.getSide() == DisplaySide.A) {
+ int comp = mapper.lineOnOther(o1.getSide(), o1.getStart())
+ .getLine() - o2.getStart();
+ return comp == 0 ? -1 : comp;
+ } else {
+ int comp = o1.getStart() -
+ mapper.lineOnOther(o2.getSide(), o2.getStart()).getLine();
+ return comp == 0 ? 1 : comp;
+ }
+ }
+ };
+ }
+
+ private DiffChunkInfo getDiffChunk(DisplaySide side, int line) {
+ int res = Collections.binarySearch(
+ diffChunks,
+ new DiffChunkInfo(side, line, 0, false), // Dummy DiffChunkInfo
+ getDiffChunkComparator());
+ if (res >= 0) {
+ return diffChunks.get(res);
+ } else { // The line might be within a DiffChunk
+ res = -res - 1;
+ if (res > 0) {
+ DiffChunkInfo info = diffChunks.get(res - 1);
+ if (info.getSide() == side && info.getStart() <= line &&
+ line <= info.getEnd()) {
+ return info;
+ }
+ }
+ }
+ return null;
+ }
+
+ private int getWrapAroundDiffChunkIndex(int index) {
+ return (index + diffChunks.size()) % diffChunks.size();
+ }
+
+ void resizePaddingOnOtherSide(DisplaySide mySide, int line) {
+ CodeMirror cm = getCmFromSide(mySide);
+ LineHandle handle = cm.getLineHandle(line);
+ final LinePaddingWidgetWrapper otherWrapper = linePaddingOnOtherSideMap.get(handle);
+ double myChunkHeight = cm.heightAtLine(line + 1) -
+ cm.heightAtLine(line - otherWrapper.getChunkLength() + 1);
+ Element otherPadding = otherWrapper.getElement();
+ int otherPaddingHeight = otherPadding.getOffsetHeight();
+ CodeMirror otherCm = otherCm(cm);
+ int otherLine = otherWrapper.getOtherLine();
+ LineHandle other = otherCm.getLineHandle(otherLine);
+ if (linePaddingOnOtherSideMap.containsKey(other)) {
+ LinePaddingWidgetWrapper myWrapper = linePaddingOnOtherSideMap.get(other);
+ Element myPadding = linePaddingOnOtherSideMap.get(other).getElement();
+ int myPaddingHeight = myPadding.getOffsetHeight();
+ myChunkHeight -= myPaddingHeight;
+ double otherChunkHeight = otherCm.heightAtLine(otherLine + 1) -
+ otherCm.heightAtLine(otherLine - myWrapper.getChunkLength() + 1) -
+ otherPaddingHeight;
+ double delta = myChunkHeight - otherChunkHeight;
+ if (delta > 0) {
+ if (myPaddingHeight != 0) {
+ setHeightInPx(myPadding, 0);
+ myWrapper.getWidget().changed();
+ }
+ if (otherPaddingHeight != delta) {
+ setHeightInPx(otherPadding, delta);
+ otherWrapper.getWidget().changed();
+ }
+ } else {
+ if (myPaddingHeight != -delta) {
+ setHeightInPx(myPadding, -delta);
+ myWrapper.getWidget().changed();
+ }
+ if (otherPaddingHeight != 0) {
+ setHeightInPx(otherPadding, 0);
+ otherWrapper.getWidget().changed();
+ }
+ }
+ } else if (otherPaddingHeight != myChunkHeight) {
+ setHeightInPx(otherPadding, myChunkHeight);
+ otherWrapper.getWidget().changed();
+ }
+ }
+
+ // TODO: Maybe integrate this with PaddingManager.
+ private RenderLineHandler resizeLinePadding(final DisplaySide side) {
+ return new RenderLineHandler() {
+ @Override
+ public void handle(final CodeMirror instance, final LineHandle handle,
+ Element element) {
+ if (lineActiveBoxMap.containsKey(handle)) {
+ lineActiveBoxMap.get(handle).resizePaddingWidget();
+ }
+ if (linePaddingOnOtherSideMap.containsKey(handle)) {
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ resizePaddingOnOtherSide(side, instance.getLineNumber(handle));
+ }
+ });
+ }
+ }
+ };
+ }
+
+ void resizeCodeMirror() {
+ if (cmA == null) {
+ return;
+ }
+ int h = Gerrit.getHeaderFooterHeight()
+ + header.getOffsetHeight()
+ + diffTable.getHeaderHeight()
+ + 10; // Estimate
+ cmA.setHeight(Window.getClientHeight() - h);
+ cmA.refresh();
+ cmB.setHeight(Window.getClientHeight() - h);
+ cmB.refresh();
+ diffTable.sidePanel.adjustGutters(cmB);
+ }
+
+ static void setHeightInPx(Element ele, double height) {
+ ele.getStyle().setHeight(height, Unit.PX);
+ }
+
+ private String getContentType(DiffInfo.FileMeta meta) {
+ return pref.isSyntaxHighlighting()
+ && meta != null
+ && meta.content_type() != null
+ ? ModeInjector.getContentType(meta.content_type())
+ : null;
+ }
+
+ CodeMirror getCmA() {
+ return cmA;
+ }
+
+ CodeMirror getCmB() {
+ return cmB;
+ }
+
+ private void prefetchNextFile() {
+ String nextPath = header.getNextPath();
+ if (nextPath != null) {
+ DiffApi.diff(revision, nextPath)
+ .base(base)
+ .wholeFile()
+ .intraline(pref.isIntralineDifference())
+ .ignoreWhitespace(pref.getIgnoreWhitespace())
+ .get(new AsyncCallback<DiffInfo>() {
+ @Override
+ public void onSuccess(DiffInfo info) {
+ new ModeInjector()
+ .add(getContentType(info.meta_a()))
+ .add(getContentType(info.meta_b()))
+ .inject(CallbackGroup.<Void> emptyCallback());
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ }
+ });
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.ui.xml
new file mode 100644
index 0000000..1bc707a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.ui.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'
+ xmlns:d='urn:import:com.google.gerrit.client.diff'>
+ <g:FlowPanel>
+ <d:Header ui:field='header'/>
+ <d:DiffTable ui:field='diffTable'/>
+ </g:FlowPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SidePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SidePanel.java
new file mode 100644
index 0000000..fb1a96f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SidePanel.java
@@ -0,0 +1,165 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Label;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.LineCharacter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** The Widget that handles the scrollbar gutters */
+class SidePanel extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, SidePanel> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+
+ interface SidePanelStyle extends CssResource {
+ String gutter();
+ String halfGutter();
+ String comment();
+ String draft();
+ String insert();
+ String delete();
+ }
+
+ enum GutterType {
+ COMMENT, DRAFT, INSERT, DELETE, EDIT;
+ }
+
+ @UiField
+ SidePanelStyle style;
+
+ private List<GutterWrapper> gutters;
+ private CodeMirror cmB;
+
+ SidePanel() {
+ initWidget(uiBinder.createAndBindUi(this));
+ this.gutters = new ArrayList<GutterWrapper>();
+ }
+
+ GutterWrapper addGutter(CodeMirror cm, int line, GutterType type) {
+ Label gutter = new Label();
+ GutterWrapper info = new GutterWrapper(this, gutter, cm, line, type);
+ adjustGutter(info);
+ gutter.addStyleName(style.gutter());
+ switch (type) {
+ case COMMENT:
+ gutter.addStyleName(style.comment());
+ break;
+ case DRAFT:
+ gutter.addStyleName(style.draft());
+ gutter.setText("*");
+ break;
+ case INSERT:
+ gutter.addStyleName(style.insert());
+ break;
+ case DELETE:
+ gutter.addStyleName(style.delete());
+ break;
+ case EDIT:
+ gutter.addStyleName(style.insert());
+ Label labelLeft = new Label();
+ labelLeft.addStyleName(style.halfGutter());
+ gutter.getElement().appendChild(labelLeft.getElement());
+ }
+ ((HTMLPanel) getWidget()).add(gutter);
+ gutters.add(info);
+ return info;
+ }
+
+ void adjustGutters(CodeMirror cmB) {
+ this.cmB = cmB;
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ for (GutterWrapper info : gutters) {
+ adjustGutter(info);
+ }
+ }
+ });
+ }
+
+ private void adjustGutter(GutterWrapper wrapper) {
+ if (cmB == null) {
+ return;
+ }
+ final CodeMirror cm = wrapper.cm;
+ final int line = wrapper.line;
+ Label gutter = wrapper.gutter;
+ final double height = cm.heightAtLine(line, "local");
+ final double scrollbarHeight = cmB.getScrollbarV().getClientHeight();
+ double top = height / (double) cmB.getSizer().getClientHeight() *
+ scrollbarHeight +
+ cmB.getScrollbarV().getAbsoluteTop();
+ if (top == 0) {
+ top = -10;
+ }
+ gutter.getElement().getStyle().setTop(top, Unit.PX);
+ wrapper.replaceClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ cm.setCursor(LineCharacter.create(line));
+ cm.scrollToY(Math.max(0, height - 0.5 * scrollbarHeight));
+ cm.focus();
+ }
+ });
+ }
+
+ void removeGutter(GutterWrapper wrapper) {
+ gutters.remove(wrapper);
+ }
+
+ static class GutterWrapper {
+ private SidePanel host;
+ private Label gutter;
+ private CodeMirror cm;
+ private int line;
+ private HandlerRegistration regClick;
+
+ GutterWrapper(SidePanel host, Label anchor, CodeMirror cm, int line,
+ GutterType type) {
+ this.host = host;
+ this.gutter = anchor;
+ this.cm = cm;
+ this.line = line;
+ }
+
+ private void replaceClickHandler(ClickHandler newHandler) {
+ if (regClick != null) {
+ regClick.removeHandler();
+ }
+ regClick = gutter.addClickHandler(newHandler);
+ }
+
+ void remove() {
+ gutter.removeFromParent();
+ host.removeGutter(this);
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SidePanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SidePanel.ui.xml
new file mode 100644
index 0000000..e2b56f3f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SidePanel.ui.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+ <ui:style type='com.google.gerrit.client.diff.SidePanel.SidePanelStyle'>
+ .gutter {
+ cursor: pointer;
+ position: fixed;
+ height: 3px;
+ width: 10px;
+ border: 1px solid black;
+ }
+ .halfGutter {
+ cursor: pointer;
+ position: fixed;
+ height: 3px;
+ width: 5px;
+ background-color: #faa;
+ }
+ .comment, .draft {
+ background-color: #e5ecf9;
+ }
+ .draft {
+ text-align: center;
+ font-size: small;
+ line-height: 0.5;
+ color: inherit !important;
+ text-decoration: none !important;
+ }
+ .delete {
+ background-color: #faa;
+ }
+ .insert {
+ background-color: #9f9;
+ }
+ </ui:style>
+ <g:HTMLPanel/>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
new file mode 100644
index 0000000..6d63e63
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
@@ -0,0 +1,193 @@
+//Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+
+import net.codemirror.lib.CodeMirror;
+import net.codemirror.lib.CodeMirror.LineClassWhere;
+import net.codemirror.lib.Configuration;
+import net.codemirror.lib.LineWidget;
+import net.codemirror.lib.TextMarker;
+import net.codemirror.lib.TextMarker.FromTo;
+
+/** The Widget that handles expanding of skipped lines */
+class SkipBar extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, SkipBar> {}
+ private static final Binder uiBinder = GWT.create(Binder.class);
+ private static final int NUM_ROWS_TO_EXPAND = 10;
+ private static final int UP_DOWN_THRESHOLD = 30;
+ private static final Configuration COLLAPSED =
+ Configuration.create().set("collapsed", true);
+
+ private LineWidget widget;
+
+ interface SkipBarStyle extends CssResource {
+ String noExpand();
+ }
+
+ @UiField(provided=true)
+ Anchor skipNum;
+
+ @UiField(provided=true)
+ Anchor upArrow;
+
+ @UiField(provided=true)
+ Anchor downArrow;
+
+ @UiField
+ SkipBarStyle style;
+
+ private TextMarker marker;
+ private SkipBar otherBar;
+ private CodeMirror cm;
+ private int numSkipLines;
+
+ SkipBar(CodeMirror cmInstance) {
+ cm = cmInstance;
+ skipNum = new Anchor(true);
+ upArrow = new Anchor(true);
+ downArrow = new Anchor(true);
+ initWidget(uiBinder.createAndBindUi(this));
+ addDomHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ cm.focus();
+ }
+ }, ClickEvent.getType());
+ }
+
+ void setWidget(LineWidget lineWidget) {
+ widget = lineWidget;
+ Scheduler.get().scheduleDeferred(new ScheduledCommand(){
+ @Override
+ public void execute() {
+ getElement().getStyle().setPaddingLeft(
+ cm.getGutterElement().getOffsetWidth(), Unit.PX);
+ }
+ });
+ }
+
+ void setMarker(TextMarker marker, int length) {
+ this.marker = marker;
+ numSkipLines = length;
+ skipNum.setText(Integer.toString(length));
+ if (checkAndUpdateArrows()) {
+ upArrow.setHTML(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND));
+ downArrow.setHTML(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND));
+ }
+ }
+
+ static void link(SkipBar barA, SkipBar barB) {
+ barA.otherBar = barB;
+ barB.otherBar = barA;
+ }
+
+ private void updateSkipNum() {
+ numSkipLines -= NUM_ROWS_TO_EXPAND;
+ skipNum.setText(String.valueOf(numSkipLines));
+ checkAndUpdateArrows();
+ }
+
+ private boolean checkAndUpdateArrows() {
+ if (numSkipLines <= UP_DOWN_THRESHOLD) {
+ addStyleName(style.noExpand());
+ return false;
+ }
+ return true;
+ }
+
+ private void clearMarkerAndWidget() {
+ marker.clear();
+ if (widget != null) {
+ widget.clear();
+ } else {
+ cm.removeLineClass(0, LineClassWhere.WRAP, DiffTable.style.hideNumber());
+ }
+ }
+
+ private void expandAll() {
+ clearMarkerAndWidget();
+ removeFromParent();
+ cm.focus();
+ }
+
+ private void expandBefore() {
+ FromTo fromTo = marker.find();
+ int oldStart = fromTo.getFrom().getLine();
+ int newStart = oldStart + NUM_ROWS_TO_EXPAND;
+ int end = fromTo.getTo().getLine();
+ clearMarkerAndWidget();
+ marker = cm.markText(CodeMirror.pos(newStart), CodeMirror.pos(end), COLLAPSED);
+ Configuration config = Configuration.create().set("coverGutter", true);
+ LineWidget newWidget = cm.addLineWidget(newStart, getElement(), config);
+ setWidget(newWidget);
+ updateSkipNum();
+ cm.focus();
+ }
+
+ private void expandAfter() {
+ FromTo fromTo = marker.find();
+ int start = fromTo.getFrom().getLine();
+ int oldEnd = fromTo.getTo().getLine();
+ int newEnd = oldEnd - NUM_ROWS_TO_EXPAND;
+ marker.clear();
+ if (widget == null) { // First line workaround
+ marker = cm.markText(CodeMirror.pos(-1),
+ CodeMirror.pos(newEnd),
+ Configuration.create()
+ .set("inclusiveLeft", true)
+ .set("inclusiveRight", true)
+ .set("replacedWith", getElement()));
+ } else {
+ marker = cm.markText(CodeMirror.pos(start),
+ CodeMirror.pos(newEnd),
+ COLLAPSED);
+ }
+ updateSkipNum();
+ cm.focus();
+ }
+
+ @UiHandler("skipNum")
+ void onExpandAll(ClickEvent e) {
+ otherBar.expandAll();
+ expandAll();
+ }
+
+ @UiHandler("upArrow")
+ void onExpandBefore(ClickEvent e) {
+ otherBar.expandBefore();
+ expandBefore();
+ }
+
+ @UiHandler("downArrow")
+ void onExpandAfter(ClickEvent e) {
+ otherBar.expandAfter();
+ expandAfter();
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
new file mode 100644
index 0000000..f5c6719
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+ <ui:style type='com.google.gerrit.client.diff.SkipBar.SkipBarStyle'>
+ .skipBar {
+ background-color: #def;
+ height: 1.3em;
+ overflow: hidden;
+ }
+ .text {
+ display: table;
+ margin: 0 auto;
+ color: #777;
+ font-style: italic;
+ overflow: hidden;
+ }
+ .anchor {
+ color: inherit;
+ text-decoration: none;
+ }
+ .noExpand .arrow {
+ display: none;
+ }
+ .arrow {
+ font-family: Arial Unicode MS, sans-serif;
+ }
+ </ui:style>
+ <g:HTMLPanel addStyleNames='{style.skipBar}'>
+ <div class='{style.text}'>
+ <ui:msg>
+ <g:Anchor ui:field='upArrow' addStyleNames='{style.arrow} {style.anchor}' />
+ <span><ui:msg>... skipped </ui:msg></span>
+ <g:Anchor ui:field='skipNum' addStyleNames='{style.anchor}' />
+ <span><ui:msg> common lines ...</ui:msg></span>
+ <g:Anchor ui:field='downArrow' addStyleNames=' {style.arrow} {style.anchor}' />
+ </ui:msg>
+ </div>
+ </g:HTMLPanel>
+</ui:UiBinder>
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand2.java
new file mode 100644
index 0000000..f40c4c1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand2.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+
+class UpToChangeCommand2 extends KeyCommand {
+ private final PatchSet.Id revision;
+
+ UpToChangeCommand2(PatchSet.Id revision, int mask, int key) {
+ super(mask, key, PatchUtil.C.upToChange());
+ this.revision = revision;
+ }
+
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ Gerrit.display(PageLinks.toChange2(
+ revision.getParentKey(),
+ String.valueOf(revision.get())));
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy100.png
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy.png
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy100.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy26.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy26.png
new file mode 100644
index 0000000..88b59d8
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diffy26.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index 5f16af6..8087b68 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -39,12 +39,15 @@
/** Override various GWT defaults */
.gerritTopMenu {
font-size: 9pt;
- padding-top: 5px;
padding-left: 5px;
padding-right: 5px;
background: transparent;
}
+body, table td, select {
+ font-family: norm-font;
+}
+
.gerritBody {
font-size: small;
padding-left: 5px;
@@ -62,12 +65,11 @@
text-decoration: underline;
}
-.version,
-.keyhelp {
+#gerrit_btmmenu {
+ clear: both;
color: #a0adcc;
- right: 0;
- padding-right: 10px;
text-align: right;
+ padding-right: 10px;
}
.version a,
@@ -339,6 +341,7 @@
border: 1px solid black;
background: white;
box-shadow: 3px 3px 5px #888;
+ z-index: 5;
}
.searchPanel {
white-space: nowrap;
@@ -352,6 +355,9 @@
margin-left: 2px;
padding: 3px 6px;
}
+.suggestBoxPopup {
+ z-index: 5;
+}
/** RPC Status **/
.rpcStatusPanel {
@@ -1534,3 +1540,11 @@
.projectFilterLabel {
margin-right: 5px;
}
+.projectNameColumn {
+ min-width: 300px;
+}
+
+/** ProjectSettings */
+.maxObjectSizeLimitPanel td {
+ padding-right: 5px;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
index 8c9c56b..c6793d6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
@@ -40,6 +40,7 @@
String illegalNumberOfColumns();
String upToChange();
+ String openReply();
String linePrev();
String lineNext();
String chunkPrev();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
index 5acdb5f..5259a4c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
@@ -22,6 +22,7 @@
illegalNumberOfColumns = The number of columns cannot be zero or negative
upToChange = Up to change
+openReply = Reply and score
linePrev = Previous line
lineNext = Next line
chunkPrev = Previous diff chunk or comment
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
index 200562a..4817ffd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
@@ -386,7 +386,7 @@
}
}));
PatchUtil.DETAIL_SVC.patchScript(patchKey, idSideA, idSideB,
- settingsPanel.getValue(), cb.addGwtjsonrpc(
+ settingsPanel.getValue(), cb.addFinal(
new ScreenLoadCallback<PatchScript>(this) {
@Override
protected void preDisplay(final PatchScript result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
index 8e76e47..9795a64 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
@@ -314,7 +314,7 @@
if (0 <= sel) {
return Short.parseShort(context.getValue(sel));
}
- return (short) getValue().getContext();
+ return getValue().getContext();
}
private void setContext(int ctx) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
index df12b70..0ac06d0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
@@ -44,7 +44,7 @@
interface Binder extends UiBinder<HTMLPanel, PatchSetSelectBox> {
}
- private static Binder uiBinder = GWT.create(Binder.class);
+ private static final Binder uiBinder = GWT.create(Binder.class);
interface BoxStyle extends CssResource {
String selected();
@@ -133,7 +133,7 @@
if (idActive == null && side == Side.A) {
links.get(0).setStyleName(style.selected());
- } else {
+ } else if (idActive != null) {
links.get(idActive.get()).setStyleName(style.selected());
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
new file mode 100644
index 0000000..3f79afe
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.projects;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class BranchInfo extends JavaScriptObject {
+ public final String getShortName() {
+ return ref().startsWith(Branch.R_HEADS)
+ ? ref().substring(Branch.R_HEADS.length())
+ : ref();
+ }
+
+ public final native String ref() /*-{ return this.ref; }-*/;
+ public final native String revision() /*-{ return this.revision; }-*/;
+ public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
+
+ protected BranchInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index 522d348..f35da6a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -15,6 +15,9 @@
package com.google.gerrit.client.projects;
import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwtexpui.safehtml.client.FindReplace;
@@ -25,26 +28,39 @@
import java.util.List;
public class ConfigInfo extends JavaScriptObject {
- public final native JavaScriptObject has_require_change_id()
- /*-{ return this.hasOwnProperty('require_change_id'); }-*/;
- public final native boolean require_change_id()
+ public final native String description()
+ /*-{ return this.description }-*/;
+
+ public final native InheritedBooleanInfo require_change_id()
/*-{ return this.require_change_id; }-*/;
- public final native JavaScriptObject has_use_content_merge()
- /*-{ return this.hasOwnProperty('use_content_merge'); }-*/;
- public final native boolean use_content_merge()
+ public final native InheritedBooleanInfo use_content_merge()
/*-{ return this.use_content_merge; }-*/;
- public final native JavaScriptObject has_use_contributor_agreements()
- /*-{ return this.hasOwnProperty('use_contributor_agreements'); }-*/;
- public final native boolean use_contributor_agreements()
+ public final native InheritedBooleanInfo use_contributor_agreements()
/*-{ return this.use_contributor_agreements; }-*/;
- public final native JavaScriptObject has_use_signed_off_by()
- /*-{ return this.hasOwnProperty('use_signed_off_by'); }-*/;
- public final native boolean use_signed_off_by()
+ public final native InheritedBooleanInfo use_signed_off_by()
/*-{ return this.use_signed_off_by; }-*/;
+ public final SubmitType submit_type() {
+ return SubmitType.valueOf(submit_typeRaw());
+ }
+ private final native String submit_typeRaw()
+ /*-{ return this.submit_type }-*/;
+
+ public final Project.State state() {
+ if (stateRaw() == null) {
+ return Project.State.ACTIVE;
+ }
+ return Project.State.valueOf(stateRaw());
+ }
+ private final native String stateRaw()
+ /*-{ return this.state }-*/;
+
+ public final native MaxObjectSizeLimitInfo max_object_size_limit()
+ /*-{ return this.max_object_size_limit; }-*/;
+
private final native NativeMap<CommentLinkInfo> commentlinks0()
/*-{ return this.commentlinks; }-*/;
final List<FindReplace> commentlinks() {
@@ -80,4 +96,40 @@
protected CommentLinkInfo() {
}
}
+
+ public static class InheritedBooleanInfo extends JavaScriptObject {
+ public static InheritedBooleanInfo create() {
+ return (InheritedBooleanInfo) createObject();
+ }
+
+ public final native boolean value()
+ /*-{ return this.value ? true : false; }-*/;
+
+ public final native boolean inherited_value()
+ /*-{ return this.inherited_value ? true : false; }-*/;
+
+ public final InheritableBoolean configured_value() {
+ return InheritableBoolean.valueOf(configured_valueRaw());
+ }
+ private final native String configured_valueRaw()
+ /*-{ return this.configured_value }-*/;
+
+ public final void setConfiguredValue(InheritableBoolean v) {
+ setConfiguredValueRaw(v.name());
+ }
+ public final native void setConfiguredValueRaw(String v)
+ /*-{ if(v)this.configured_value=v; }-*/;
+
+ protected InheritedBooleanInfo() {
+ }
+ }
+
+ public static class MaxObjectSizeLimitInfo extends JavaScriptObject {
+ public final native String value() /*-{ return this.value; }-*/;
+ public final native String inherited_value() /*-{ return this.inherited_value; }-*/;
+ public final native String configured_value() /*-{ return this.configured_value }-*/;
+
+ protected MaxObjectSizeLimitInfo() {
+ }
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
index 16406f4..c22b007 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
@@ -14,7 +14,10 @@
package com.google.gerrit.client.projects;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -24,7 +27,8 @@
/** Cache of {@link ConfigInfo} objects by project name. */
public class ConfigInfoCache {
- private static final int LIMIT = 25;
+ private static final int PROJECT_LIMIT = 25;
+ private static final int CHANGE_LIMIT = 100;
private static final ConfigInfoCache instance =
GWT.create(ConfigInfoCache.class);
@@ -49,36 +53,74 @@
}
public static void get(Project.NameKey name, AsyncCallback<Entry> cb) {
- instance.getImpl(name, cb);
+ instance.getImpl(name.get(), cb);
+ }
+
+ public static void get(Change.Id changeId, AsyncCallback<Entry> cb) {
+ instance.getImpl(changeId.get(), cb);
+ }
+
+ public static void add(ChangeInfo info) {
+ instance.changeToProject.put(info.legacy_id().get(), info.project());
}
private final LinkedHashMap<String, Entry> cache;
+ private final LinkedHashMap<Integer, String> changeToProject;
protected ConfigInfoCache() {
- cache = new LinkedHashMap<String, Entry>(LIMIT) {
+ cache = new LinkedHashMap<String, Entry>(PROJECT_LIMIT) {
private static final long serialVersionUID = 1L;
@Override
protected boolean removeEldestEntry(
Map.Entry<String, ConfigInfoCache.Entry> e) {
- return size() > LIMIT;
+ return size() > PROJECT_LIMIT;
+ }
+ };
+
+ changeToProject = new LinkedHashMap<Integer, String>(CHANGE_LIMIT) {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<Integer, String> e) {
+ return size() > CHANGE_LIMIT;
}
};
}
- private void getImpl(final Project.NameKey name,
- final AsyncCallback<Entry> cb) {
- Entry e = cache.get(name.get());
+ private void getImpl(final String name, final AsyncCallback<Entry> cb) {
+ Entry e = cache.get(name);
if (e != null) {
cb.onSuccess(e);
return;
}
- ProjectApi.config(name).get(new AsyncCallback<ConfigInfo>() {
+ ProjectApi.getConfig(new Project.NameKey(name),
+ new AsyncCallback<ConfigInfo>() {
+ @Override
+ public void onSuccess(ConfigInfo result) {
+ Entry e = new Entry(result);
+ cache.put(name, e);
+ cb.onSuccess(e);
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ cb.onFailure(caught);
+ }
+ });
+ }
+
+ private void getImpl(final Integer id, final AsyncCallback<Entry> cb) {
+ String name = changeToProject.get(id);
+ if (name != null) {
+ getImpl(name, cb);
+ return;
+ }
+ ChangeApi.change(id).get(new AsyncCallback<ChangeInfo>() {
@Override
- public void onSuccess(ConfigInfo result) {
- Entry e = new Entry(result);
- cache.put(name.get(), e);
- cb.onSuccess(e);
+ public void onSuccess(ChangeInfo result) {
+ changeToProject.put(id, result.project());
+ getImpl(result.project(), cb);
}
@Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index be133c5..503d85b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -14,27 +14,121 @@
package com.google.gerrit.client.projects;
import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
import com.google.gwt.user.client.rpc.AsyncCallback;
+import java.util.Set;
+
public class ProjectApi {
/** Create a new project */
public static void createProject(String projectName, String parent,
Boolean createEmptyCcommit, Boolean permissionsOnly,
- AsyncCallback<VoidResult> asyncCallback) {
+ AsyncCallback<VoidResult> cb) {
ProjectInput input = ProjectInput.create();
input.setName(projectName);
input.setParent(parent);
input.setPermissionsOnly(permissionsOnly);
input.setCreateEmptyCommit(createEmptyCcommit);
new RestApi("/projects/").id(projectName).ifNoneMatch()
- .put(input, asyncCallback);
+ .put(input, cb);
}
- static RestApi config(Project.NameKey name) {
- return new RestApi("/projects/").id(name.get()).view("config");
+ /** Create a new branch */
+ public static void createBranch(Project.NameKey name, String ref,
+ String revision, AsyncCallback<BranchInfo> cb) {
+ BranchInput input = BranchInput.create();
+ input.setRevision(revision);
+ project(name).view("branches").id(ref).ifNoneMatch().put(input, cb);
+ }
+
+ /** Retrieve all visible branches of the project */
+ public static void getBranches(Project.NameKey name,
+ AsyncCallback<JsArray<BranchInfo>> cb) {
+ project(name).view("branches").get(cb);
+ }
+
+ /**
+ * Delete branches. For each branch to be deleted a separate DELETE request is
+ * fired to the server. The {@code onSuccess} method of the provided callback
+ * is invoked once after all requests succeeded. If any request fails the
+ * callbacks' {@code onFailure} method is invoked. In a failure case it can be
+ * that still some of the branches were successfully deleted.
+ */
+ public static void deleteBranches(Project.NameKey name,
+ Set<String> refs, AsyncCallback<VoidResult> cb) {
+ CallbackGroup group = new CallbackGroup();
+ for (String ref : refs) {
+ project(name).view("branches").id(ref)
+ .delete(group.add(cb));
+ cb = CallbackGroup.emptyCallback();
+ }
+ group.done();
+ }
+
+ public static void getConfig(Project.NameKey name,
+ AsyncCallback<ConfigInfo> cb) {
+ project(name).view("config").get(cb);
+ }
+
+ public static void setConfig(Project.NameKey name, String description,
+ InheritableBoolean useContributorAgreements,
+ InheritableBoolean useContentMerge, InheritableBoolean useSignedOffBy,
+ InheritableBoolean requireChangeId, String maxObjectSizeLimit,
+ SubmitType submitType, Project.State state, AsyncCallback<ConfigInfo> cb) {
+ ConfigInput in = ConfigInput.create();
+ in.setDescription(description);
+ in.setUseContributorAgreements(useContributorAgreements);
+ in.setUseContentMerge(useContentMerge);
+ in.setUseSignedOffBy(useSignedOffBy);
+ in.setRequireChangeId(requireChangeId);
+ in.setMaxObjectSizeLimit(maxObjectSizeLimit);
+ in.setSubmitType(submitType);
+ in.setState(state);
+ project(name).view("config").put(in, cb);
+ }
+
+ public static void getParent(Project.NameKey name,
+ final AsyncCallback<Project.NameKey> cb) {
+ project(name).view("parent").get(
+ new AsyncCallback<NativeString>() {
+ @Override
+ public void onSuccess(NativeString result) {
+ cb.onSuccess(new Project.NameKey(result.asString()));
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ cb.onFailure(caught);
+ }
+ });
+ }
+
+ public static void getDescription(Project.NameKey name,
+ AsyncCallback<NativeString> cb) {
+ project(name).view("description").get(cb);
+ }
+
+ public static void setDescription(Project.NameKey name, String description,
+ AsyncCallback<NativeString> cb) {
+ RestApi call = project(name).view("description");
+ if (description != null && !description.isEmpty()) {
+ DescriptionInput input = DescriptionInput.create();
+ input.setDescription(description);
+ call.put(input, cb);
+ } else {
+ call.delete(cb);
+ }
+ }
+
+ private static RestApi project(Project.NameKey name) {
+ return new RestApi("/projects/").id(name.get());
}
private static class ProjectInput extends JavaScriptObject {
@@ -53,4 +147,77 @@
final native void setCreateEmptyCommit(boolean cc) /*-{ if(cc)this.create_empty_commit=cc; }-*/;
}
+
+ private static class ConfigInput extends JavaScriptObject {
+ static ConfigInput create() {
+ return (ConfigInput) createObject();
+ }
+
+ protected ConfigInput() {
+ }
+
+ final native void setDescription(String d)
+ /*-{ if(d)this.description=d; }-*/;
+
+ final void setUseContributorAgreements(InheritableBoolean v) {
+ setUseContributorAgreementsRaw(v.name());
+ }
+ private final native void setUseContributorAgreementsRaw(String v)
+ /*-{ if(v)this.use_contributor_agreements=v; }-*/;
+
+ final void setUseContentMerge(InheritableBoolean v) {
+ setUseContentMergeRaw(v.name());
+ }
+ private final native void setUseContentMergeRaw(String v)
+ /*-{ if(v)this.use_content_merge=v; }-*/;
+
+ final void setUseSignedOffBy(InheritableBoolean v) {
+ setUseSignedOffByRaw(v.name());
+ }
+ private final native void setUseSignedOffByRaw(String v)
+ /*-{ if(v)this.use_signed_off_by=v; }-*/;
+
+ final void setRequireChangeId(InheritableBoolean v) {
+ setRequireChangeIdRaw(v.name());
+ }
+ private final native void setRequireChangeIdRaw(String v)
+ /*-{ if(v)this.require_change_id=v; }-*/;
+
+ final native void setMaxObjectSizeLimit(String l)
+ /*-{ if(l)this.max_object_size_limit=l; }-*/;
+
+ final void setSubmitType(SubmitType t) {
+ setSubmitTypeRaw(t.name());
+ }
+ private final native void setSubmitTypeRaw(String t)
+ /*-{ if(t)this.submit_type=t; }-*/;
+
+ final void setState(Project.State s) {
+ setStateRaw(s.name());
+ }
+ private final native void setStateRaw(String s)
+ /*-{ if(s)this.state=s; }-*/;
+ }
+
+ private static class BranchInput extends JavaScriptObject {
+ static BranchInput create() {
+ return (BranchInput) createObject();
+ }
+
+ protected BranchInput() {
+ }
+
+ final native void setRevision(String r) /*-{ if(r)this.revision=r; }-*/;
+ }
+
+ private static class DescriptionInput extends JavaScriptObject {
+ static DescriptionInput create() {
+ return (DescriptionInput) createObject();
+ }
+
+ protected DescriptionInput() {
+ }
+
+ final native void setDescription(String d) /*-{ if(d)this.description=d; }-*/;
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java
index 67b6077..9a852a2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ThemeInfo.java
@@ -10,7 +10,7 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
-// limitations under the License.package com.google.gerrit.server.git;
+// limitations under the License.
package com.google.gerrit.client.projects;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
index 0b1f905..073c949 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
@@ -17,15 +17,18 @@
import com.google.gwt.user.client.rpc.AsyncCallback;
import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
-import java.util.Map;
+import java.util.Set;
/**
* Class for grouping together callbacks and calling them in order.
* <p>
* Callbacks are added to the group with {@link #add(AsyncCallback)}, which
* returns a wrapped callback suitable for passing to an asynchronous RPC call.
+ * The last callback must be added using {@link #addFinal(AsyncCallback)} or
+ * {@link #done()} must be invoked.
+ *
* The enclosing group buffers returned results and ensures that
* {@code onSuccess} is called exactly once for each callback in the group, in
* the same order that callbacks were added. This allows callers to, for
@@ -39,82 +42,119 @@
* processing it.
*/
public class CallbackGroup {
- private final List<Object> callbacks;
- private final Map<Object, Object> results;
+ private final List<CallbackImpl<?>> callbacks;
+ private final Set<CallbackImpl<?>> remaining;
+ private boolean finalAdded;
+
private boolean failed;
+ private Throwable failedThrowable;
+
+ public static <T> Callback<T> emptyCallback() {
+ return new Callback<T>() {
+ @Override
+ public void onSuccess(T result) {
+ }
+
+ @Override
+ public void onFailure(Throwable err) {
+ }
+ };
+ }
public CallbackGroup() {
- callbacks = new ArrayList<Object>();
- results = new HashMap<Object, Object>();
+ callbacks = new ArrayList<CallbackImpl<?>>();
+ remaining = new HashSet<CallbackImpl<?>>();
}
- public <T> AsyncCallback<T> add(final AsyncCallback<T> cb) {
- callbacks.add(cb);
- return new AsyncCallback<T>() {
- @Override
- public void onSuccess(T result) {
- results.put(cb, result);
- CallbackGroup.this.onSuccess();
- }
-
- @Override
- public void onFailure(Throwable caught) {
- CallbackGroup.this.onFailure(caught);
- }
- };
+ public <T> Callback<T> add(final AsyncCallback<T> cb) {
+ checkFinalAdded();
+ return handleAdd(cb);
}
- public <T> com.google.gwtjsonrpc.common.AsyncCallback<T> addGwtjsonrpc(
- final com.google.gwtjsonrpc.common.AsyncCallback<T> cb) {
- callbacks.add(cb);
- return new com.google.gwtjsonrpc.common.AsyncCallback<T>() {
- @Override
- public void onSuccess(T result) {
- results.put(cb, result);
- CallbackGroup.this.onSuccess();
- }
-
- @Override
- public void onFailure(Throwable caught) {
- CallbackGroup.this.onFailure(caught);
- }
- };
+ public <T> Callback<T> addFinal(final AsyncCallback<T> cb) {
+ checkFinalAdded();
+ finalAdded = true;
+ return handleAdd(cb);
}
- private void onSuccess() {
- if (results.size() < callbacks.size()) {
- return;
- }
- for (Object o : callbacks) {
- Object result = results.get(o);
- if (o instanceof AsyncCallback) {
- @SuppressWarnings("unchecked")
- AsyncCallback<Object> cb = (AsyncCallback<Object>) o;
- cb.onSuccess(result);
- } else {
- @SuppressWarnings("unchecked")
- com.google.gwtjsonrpc.common.AsyncCallback<Object> cb =
- (com.google.gwtjsonrpc.common.AsyncCallback<Object>) o;
- cb.onSuccess(result);
+ public void done() {
+ finalAdded = true;
+ applyAllSuccess();
+ }
+
+ private void applyAllSuccess() {
+ if (!failed && finalAdded && remaining.isEmpty()) {
+ for (CallbackImpl<?> cb : callbacks) {
+ cb.applySuccess();
}
+ callbacks.clear();
}
}
- private void onFailure(Throwable caught) {
+ private <T> Callback<T> handleAdd(AsyncCallback<T> cb) {
if (failed) {
- return;
+ cb.onFailure(failedThrowable);
+ return emptyCallback();
}
- failed = true;
- for (Object o : callbacks) {
- if (o instanceof AsyncCallback) {
- @SuppressWarnings("unchecked")
- AsyncCallback<Object> cb = (AsyncCallback<Object>) o;
- cb.onFailure(caught);
- } else {
- @SuppressWarnings("unchecked")
- com.google.gwtjsonrpc.common.AsyncCallback<Object> cb =
- (com.google.gwtjsonrpc.common.AsyncCallback<Object>) o;
- cb.onFailure(caught);
+
+ CallbackImpl<T> wrapper = new CallbackImpl<T>(cb);
+ callbacks.add(wrapper);
+ remaining.add(wrapper);
+ return wrapper;
+ }
+
+ private void checkFinalAdded() {
+ if (finalAdded) {
+ throw new IllegalStateException("final callback already added");
+ }
+ }
+
+ public interface Callback<T>
+ extends AsyncCallback<T>, com.google.gwtjsonrpc.common.AsyncCallback<T> {
+ }
+
+ private class CallbackImpl<T> implements Callback<T> {
+ AsyncCallback<T> delegate;
+ T result;
+
+ CallbackImpl(AsyncCallback<T> delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void onSuccess(T value) {
+ if (failed) {
+ return;
+ }
+
+ this.result = value;
+ remaining.remove(this);
+ CallbackGroup.this.applyAllSuccess();
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ if (failed) {
+ return;
+ }
+
+ failed = true;
+ failedThrowable = caught;
+ for (CallbackImpl<?> cb : callbacks) {
+ cb.delegate.onFailure(failedThrowable);
+ cb.delegate = null;
+ cb.result = null;
+ }
+ callbacks.clear();
+ remaining.clear();
+ }
+
+ void applySuccess() {
+ AsyncCallback<T> cb = delegate;
+ if (cb != null) {
+ delegate = null;
+ cb.onSuccess(result);
+ result = null;
}
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
index d56eeaf..2e407d4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
@@ -22,6 +22,10 @@
/** A map of native JSON objects, keyed by a string. */
public class NativeMap<T extends JavaScriptObject> extends JavaScriptObject {
+ public static <T extends JavaScriptObject> NativeMap<T> create() {
+ return createObject().cast();
+ }
+
/**
* Loop through the result map's entries and copy the key strings into the
* "name" property of the corresponding child object. This only runs on the
@@ -80,6 +84,7 @@
}
public final native T get(String n) /*-{ return this[n]; }-*/;
+ public final native void put(String n, T v) /*-{ this[n] = v; }-*/;
public final native void copyKeysIntoChildren(String p)
/*-{
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeString.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeString.java
index 573c5e7..be4cfd6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeString.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeString.java
@@ -19,12 +19,18 @@
/** Wraps a String that was returned from a JSON API. */
public final class NativeString extends JavaScriptObject {
- static NativeString wrap(String value) {
- NativeString ns = (NativeString) createObject();
- ns.set(value);
- return ns;
+ private static final JavaScriptObject TYPE = init();
+
+ private static final native JavaScriptObject init()
+ /*-{ return function(s){this.s=s} }-*/;
+
+ static final NativeString wrap(String s) {
+ return wrap0(TYPE, s);
}
+ private static final native NativeString wrap0(JavaScriptObject T, String s)
+ /*-{ return new T(s) }-*/;
+
public final native String asString() /*-{ return this.s; }-*/;
private final native void set(String v) /*-{ this.s = v; }-*/;
@@ -43,6 +49,13 @@
};
}
+ public static final boolean is(JavaScriptObject o) {
+ return is(TYPE, o);
+ }
+
+ private static final native boolean is(JavaScriptObject T, JavaScriptObject o)
+ /*-{ return o instanceof T }-*/;
+
protected NativeString() {
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
index 4ee63c6..4666d34 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
@@ -93,6 +93,7 @@
case 405: // Method Not Allowed
case 409: // Conflict
case 412: // Precondition Failed
+ case 422: // Unprocessable Entity
case 429: // Too Many Requests (RFC 6585)
return true;
@@ -105,9 +106,11 @@
private static class HttpCallback<T extends JavaScriptObject>
implements RequestCallback {
+ private final boolean background;
private final AsyncCallback<T> cb;
- HttpCallback(AsyncCallback<T> cb) {
+ HttpCallback(boolean bg, AsyncCallback<T> cb) {
+ this.background = bg;
this.cb = cb;
}
@@ -116,11 +119,15 @@
int status = res.getStatusCode();
if (status == Response.SC_NO_CONTENT) {
cb.onSuccess(null);
- RpcStatus.INSTANCE.onRpcComplete();
+ if (!background) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ }
} else if (200 <= status && status < 300) {
if (!isJsonBody(res)) {
- RpcStatus.INSTANCE.onRpcComplete();
+ if (!background) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ }
cb.onFailure(new StatusCodeException(SC_BAD_RESPONSE, "Expected "
+ JSON_TYPE + "; received Content-Type: "
+ res.getHeader("Content-Type")));
@@ -132,14 +139,18 @@
// javac generics bug
data = RestApi.<T>cast(parseJson(res));
} catch (JSONException e) {
- RpcStatus.INSTANCE.onRpcComplete();
+ if (!background) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ }
cb.onFailure(new StatusCodeException(SC_BAD_RESPONSE,
"Invalid JSON: " + e.getMessage()));
return;
}
cb.onSuccess(data);
- RpcStatus.INSTANCE.onRpcComplete();
+ if (!background) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ }
} else {
String msg;
@@ -161,14 +172,18 @@
msg = res.getStatusText();
}
- RpcStatus.INSTANCE.onRpcComplete();
+ if (!background) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ }
cb.onFailure(new StatusCodeException(status, msg));
}
}
@Override
public void onError(Request req, Throwable err) {
- RpcStatus.INSTANCE.onRpcComplete();
+ if (!background) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ }
if (err.getMessage().contains("XmlHttpRequest.status")) {
cb.onFailure(new StatusCodeException(
SC_UNAVAILABLE,
@@ -181,6 +196,7 @@
private StringBuilder url;
private boolean hasQueryParams;
+ private boolean background;
private String ifNoneMatch;
/**
@@ -275,6 +291,11 @@
return this;
}
+ public RestApi background() {
+ background = true;
+ return this;
+ }
+
public String url() {
return url.toString();
}
@@ -289,9 +310,11 @@
private <T extends JavaScriptObject> void send(
Method method, AsyncCallback<T> cb) {
- HttpCallback<T> httpCallback = new HttpCallback<T>(cb);
+ HttpCallback<T> httpCallback = new HttpCallback<T>(background, cb);
try {
- RpcStatus.INSTANCE.onRpcStart();
+ if (!background) {
+ RpcStatus.INSTANCE.onRpcStart();
+ }
request(method).sendRequest(null, httpCallback);
} catch (RequestException e) {
httpCallback.onError(null, e);
@@ -304,6 +327,11 @@
sendJSON(POST, content, cb);
}
+ public <T extends JavaScriptObject> void post(String content,
+ AsyncCallback<T> cb) {
+ sendRaw(POST, content, cb);
+ }
+
public <T extends JavaScriptObject> void put(AsyncCallback<T> cb) {
send(PUT, cb);
}
@@ -317,9 +345,11 @@
private <T extends JavaScriptObject> void sendJSON(
Method method, JavaScriptObject content,
AsyncCallback<T> cb) {
- HttpCallback<T> httpCallback = new HttpCallback<T>(cb);
+ HttpCallback<T> httpCallback = new HttpCallback<T>(background, cb);
try {
- RpcStatus.INSTANCE.onRpcStart();
+ if (!background) {
+ RpcStatus.INSTANCE.onRpcStart();
+ }
String body = new JSONObject(content).toString();
RequestBuilder req = request(method);
req.setHeader("Content-Type", JSON_UTF8);
@@ -329,6 +359,21 @@
}
}
+ private <T extends JavaScriptObject> void sendRaw(Method method, String body,
+ AsyncCallback<T> cb) {
+ HttpCallback<T> httpCallback = new HttpCallback<T>(background, cb);
+ try {
+ if (!background) {
+ RpcStatus.INSTANCE.onRpcStart();
+ }
+ RequestBuilder req = request(method);
+ req.setHeader("Content-Type", TEXT_TYPE);
+ req.sendRequest(body, httpCallback);
+ } catch (RequestException e) {
+ httpCallback.onError(null, e);
+ }
+ }
+
private RequestBuilder request(Method method) {
RequestBuilder req = new RequestBuilder(method, url());
if (ifNoneMatch != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ActionDialog.java
new file mode 100644
index 0000000..295842c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ActionDialog.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeDetailCache;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gwt.user.client.ui.FocusWidget;
+
+public abstract class ActionDialog extends CommentedActionDialog<ChangeDetail> {
+ public ActionDialog(final FocusWidget enableOnFailure, final boolean redirect,
+ String dialogTitle, String dialogHeading) {
+ super(dialogTitle, dialogHeading, new ChangeDetailCache.IgnoreErrorCallback() {
+ @Override
+ public void onSuccess(ChangeDetail result) {
+ if (redirect) {
+ Gerrit.display(PageLinks.toChange(result.getChange().getId()));
+ } else {
+ super.onSuccess(result);
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ enableOnFailure.setEnabled(true);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
index d708286..95509d8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
@@ -15,7 +15,6 @@
package com.google.gerrit.client.ui;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeScreen;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.ChangeInfo;
import com.google.gerrit.reviewdb.client.Change;
@@ -57,14 +56,6 @@
@Override
public void go() {
- Gerrit.display(getTargetHistoryToken(), createScreen());
- }
-
- private Screen createScreen() {
- if (psid != null) {
- return new ChangeScreen(psid);
- } else {
- return new ChangeScreen(cid);
- }
+ Gerrit.display(getTargetHistoryToken());
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
new file mode 100644
index 0000000..0fdec31
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.projects.BranchInfo;
+import com.google.gerrit.client.projects.ProjectApi;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusWidget;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public abstract class CherryPickDialog extends ActionDialog {
+ private SuggestBox newBranch;
+ private List<BranchInfo> branches;
+
+ public CherryPickDialog(final FocusWidget enableOnFailure, Project.NameKey project) {
+ super(enableOnFailure, true, Util.C.cherryPickTitle(), Util.C
+ .cherryPickCommitMessage());
+ ProjectApi.getBranches(project,
+ new GerritCallback<JsArray<BranchInfo>>() {
+ @Override
+ public void onSuccess(JsArray<BranchInfo> result) {
+ branches = Natives.asList(result);
+ }
+ });
+
+ newBranch = new SuggestBox(new HighlightSuggestOracle() {
+ @Override
+ protected void onRequestSuggestions(Request request, Callback done) {
+ LinkedList<BranchSuggestion> suggestions =
+ new LinkedList<BranchSuggestion>();
+ for (final BranchInfo b : branches) {
+ if (b.ref().indexOf(request.getQuery()) >= 0) {
+ suggestions.add(new BranchSuggestion(b));
+ }
+ }
+ done.onSuggestionsReady(request, new Response(suggestions));
+ }
+ });
+
+ newBranch.setWidth("100%");
+ DOM.setStyleAttribute(newBranch.getElement(), "boxSizing", "border-box");
+ message.setCharacterWidth(70);
+
+ final FlowPanel mwrap = new FlowPanel();
+ mwrap.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
+ mwrap.add(newBranch);
+
+ panel.insert(mwrap, 0);
+ panel.insert(new SmallHeading(Util.C.headingCherryPickBranch()), 0);
+ }
+
+ @Override
+ public void center() {
+ super.center();
+ GlobalKey.dialog(this);
+ newBranch.setFocus(true);
+ }
+
+ public String getDestinationBranch() {
+ return newBranch.getText();
+ }
+
+ class BranchSuggestion implements Suggestion {
+ private BranchInfo branch;
+
+ public BranchSuggestion(BranchInfo branch) {
+ this.branch = branch;
+ }
+
+ @Override
+ public String getDisplayString() {
+ return branch.ref();
+ }
+
+ @Override
+ public String getReplacementString() {
+ return branch.getShortName();
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
index 05c6b5a..bb50b19c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
@@ -50,6 +50,7 @@
HasFocusHandlers, FocusHandler, HasBlurHandlers, BlurHandler {
private static final int SUMMARY_LENGTH = 75;
private final HandlerManager handlerManager = new HandlerManager(this);
+ private final FlowPanel body;
private final FlexTable header;
private final InlineLabel messageSummary;
private final FlowPanel content;
@@ -73,7 +74,7 @@
protected CommentPanel(CommentLinkProcessor commentLinkProcessor) {
this.commentLinkProcessor = commentLinkProcessor;
- final FlowPanel body = new FlowPanel();
+ body = new FlowPanel();
initWidget(body);
setStyleName(Gerrit.RESOURCES.css().commentPanel());
@@ -127,8 +128,10 @@
}
public void setAuthorNameText(final AccountInfo author, final String nameText) {
- header.setWidget(0, 0, new AvatarImage(author, 26));
+ header.setWidget(0, 0, new AvatarImage(author));
header.setText(0, 1, nameText);
+ body.getElement().setAttribute("email", author.email());
+ body.getElement().setAttribute("name", author.name());
}
protected void setDateText(final String dateText) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
index 263703e..4ce4aa4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
@@ -67,7 +67,7 @@
});
cancelButton = new Button(Util.C.commentedActionButtonCancel());
- DOM.setStyleAttribute(cancelButton.getElement(), "marginLeft", "300px");
+ DOM.setStyleAttribute(cancelButton.getElement(), "float", "right");
cancelButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
@@ -82,6 +82,7 @@
buttonPanel = new FlowPanel();
buttonPanel.add(sendButton);
buttonPanel.add(cancelButton);
+ DOM.setStyleAttribute(buttonPanel.getElement(), "marginTop", "4px");
panel = new FlowPanel();
panel.add(new SmallHeading(heading));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
index 1edb8fd..0793527 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
@@ -25,6 +25,7 @@
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.safehtml.client.SafeHtml;
+import java.util.Comparator;
import java.util.Iterator;
public abstract class FancyFlexTable<RowItem> extends Composite {
@@ -58,6 +59,71 @@
setRowItem(table.getCellFormatter().getElement(row, 0), item);
}
+ /**
+ * Finds an item in the table.
+ *
+ * @param comparator comparator by which the items in the table are sorted
+ * @param item the item that should be found
+ * @return if the item is found the number of the row that contains the item;
+ * if the item is not found <code>-1</code>
+ */
+ protected int findRowItem(Comparator<RowItem> comparator, RowItem item) {
+ int row = lookupRowItem(comparator, item);
+ if (row < table.getRowCount()
+ && comparator.compare(item, getRowItem(row)) == 0) {
+ return row;
+ }
+ return -1;
+ }
+
+ /**
+ * Finds the number of the row where a new item should be inserted into the
+ * table.
+ *
+ * @param comparator comparator by which the items in the table are sorted
+ * @param item the new item that should be inserted
+ * @return if the item is not yet contained in the table, the number of the
+ * row where the new item should be inserted; if the item is already
+ * contained in the table <code>-1</code>
+ */
+ protected int getInsertRow(Comparator<RowItem> comparator, RowItem item) {
+ int row = lookupRowItem(comparator, item);
+ if (row >= table.getRowCount()
+ || comparator.compare(item, getRowItem(row)) != 0) {
+ return row;
+ }
+ return -1;
+ }
+
+ /**
+ * Makes a binary search for the given row item over the table.
+ *
+ * @param comparator comparator by which the items in the table are sorted
+ * @param item the item that should be looked up
+ * @return if the item is found the number of the row that contains the item;
+ * if the item is not found the number of the row where the item
+ * should be inserted according to the given comparator.
+ */
+ private int lookupRowItem(Comparator<RowItem> comparator, RowItem item) {
+ int left = 1;
+ int right = table.getRowCount() - 1;
+ while (left <= right) {
+ int middle = (left + right) >>> 1; // (left+right)/2
+ RowItem i = getRowItem(middle);
+ int cmp = comparator.compare(i, item);
+
+ if (cmp < 0) {
+ left = middle + 1;
+ } else if (cmp > 0) {
+ right = middle - 1;
+ } else {
+ // item is already contained in the table
+ return middle;
+ }
+ }
+ return left;
+ }
+
protected void resetHtml(final SafeHtml body) {
for (final Iterator<Widget> i = table.iterator(); i.hasNext();) {
i.next();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java
index c72969e..2836e0f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java
@@ -14,14 +14,14 @@
package com.google.gerrit.client.ui;
-import com.google.gerrit.client.ui.FancyFlexTable.MyFlexTable;
import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HTMLTable;
import com.google.gwtexpui.safehtml.client.SafeHtml;
public class FancyFlexTableImpl {
- public void resetHtml(final MyFlexTable myTable, final SafeHtml body) {
- SafeHtml.set(getBodyElement(myTable), body);
+ public void resetHtml(final FlexTable myTable, final SafeHtml body) {
+ SafeHtml.setInnerHTML(getBodyElement(myTable), body);
}
protected static native Element getBodyElement(HTMLTable myTable)
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE6.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE6.java
index 17e8ddd..34b6bee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE6.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE6.java
@@ -14,16 +14,16 @@
package com.google.gerrit.client.ui;
-import com.google.gerrit.client.ui.FancyFlexTable.MyFlexTable;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HTMLTable;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
public class FancyFlexTableImplIE6 extends FancyFlexTableImpl {
@Override
- public void resetHtml(final MyFlexTable myTable, final SafeHtml bodyHtml) {
+ public void resetHtml(final FlexTable myTable, final SafeHtml bodyHtml) {
final Element oldBody = getBodyElement(myTable);
final Element newBody = parseBody(bodyHtml);
assert newBody != null;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java
index 8f64887..e8a50a4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java
@@ -33,6 +33,10 @@
super(text, token);
}
+ /** Creates an empty link. */
+ public InlineHyperlink() {
+ }
+
@Override
public void onBrowserEvent(final Event event) {
if (DOM.eventGetType(event) == Event.ONCLICK && impl.handleAsClick(event)) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
index 63c2636..788c977 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
@@ -22,6 +22,7 @@
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.ScrollPanel;
@@ -195,11 +196,12 @@
final Element tr = DOM.getParent(fmt.getElement(currentRow, C_ARROW));
UIObject.setStyleName(tr, Gerrit.RESOURCES.css().activeRow(), false);
}
- if (newRow >= 0) {
+ if (0 <= newRow && newRow < table.getRowCount()
+ && getRowItem(newRow) != null) {
table.setWidget(newRow, C_ARROW, pointer);
final Element tr = DOM.getParent(fmt.getElement(newRow, C_ARROW));
UIObject.setStyleName(tr, Gerrit.RESOURCES.css().activeRow(), true);
- if (scroll) {
+ if (scroll && isAttached()) {
scrollIntoView(tr);
}
} else if (clear) {
@@ -230,7 +232,38 @@
}
});
} else {
- tr.scrollIntoView();
+ int rt = tr.getAbsoluteTop();
+ int rl = tr.getAbsoluteLeft();
+ int rb = tr.getAbsoluteBottom();
+
+ int wt = Window.getScrollTop();
+ int wl = Window.getScrollLeft();
+
+ int wh = Window.getClientHeight();
+ int ww = Window.getClientWidth();
+ int wb = wt + wh;
+
+ // If the row is partially or fully obscured, scroll:
+ //
+ // rl < wl: Row left edge is off screen to left.
+ // rt < wt: Row top is above top of window.
+ // wb < rt: Row top is below bottom of window.
+ // wb < rb: Row bottom is below bottom of window.
+ if (rl < wl || rt < wt || wb < rt || wb < rb) {
+ if (rl < wl) {
+ // Left edge needs to move to make it visible.
+ // If the row fully fits in the window, set 0.
+ if (tr.getAbsoluteRight() < ww) {
+ wl = 0;
+ } else {
+ wl = Math.max(tr.getAbsoluteLeft() - 5, 0);
+ }
+ }
+
+ // Vertically center the row in the window.
+ int h = (wh - (rb - rt)) / 2;
+ Window.scrollTo(wl, Math.max(rt - h, 0));
+ }
}
}
@@ -255,12 +288,15 @@
}
@Override
- protected void resetHtml(SafeHtml body) {
+ public void resetHtml(SafeHtml body) {
currentRow = -1;
super.resetHtml(body);
}
public void finishDisplay() {
+ if (currentRow >= table.getRowCount()) {
+ currentRow = -1;
+ }
if (saveId != null) {
movePointerTo(savedPositions.get(saveId));
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
index a7d49a4..8082527 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
@@ -47,6 +47,7 @@
private final FocusWidget widget;
private Map<TextBoxBase, String> strings = new HashMap<TextBoxBase, String>();
+ private String originalValue;
// The first parameter to the contructors must be the FocusWidget to enable,
@@ -54,6 +55,7 @@
public OnEditEnabler(final FocusWidget w, final TextBoxBase tb) {
this(w);
+ originalValue = tb.getValue().trim();
listenTo(tb);
}
@@ -75,7 +77,7 @@
// Register input widgets to be listened to
public void listenTo(final TextBoxBase tb) {
- strings.put(tb, tb.getText());
+ strings.put(tb, tb.getText().trim());
tb.addKeyPressHandler(this);
// Is there another way to capture middle button X11 pastes in browsers
@@ -89,7 +91,7 @@
tb.addFocusHandler(new FocusHandler() {
@Override
public void onFocus(FocusEvent event) {
- strings.put(tb, tb.getText());
+ strings.put(tb, tb.getText().trim());
}
});
@@ -145,7 +147,7 @@
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
- if (box.getValue().trim().length() == 0) {
+ if (box.getValue().trim().equals(originalValue)) {
widget.setEnabled(false);
}
}
@@ -173,7 +175,7 @@
if (orig == null) {
orig = "";
}
- if (! orig.equals(tb.getText())) {
+ if (! orig.equals(tb.getText().trim())) {
widget.setEnabled(true);
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
index a3a5052..052878b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
@@ -76,6 +76,7 @@
final FlexCellFormatter fmt = table.getFlexCellFormatter();
fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
+ fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().projectNameColumn());
fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
populate(row, k);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
index a26db05..0cabdda 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
@@ -48,6 +48,7 @@
protected Screen() {
initWidget(new FlowPanel());
setStyleName(Gerrit.RESOURCES.css().screen());
+ body = new FlowPanel();
}
@Override
@@ -76,7 +77,7 @@
protected void onInitUI() {
final FlowPanel me = (FlowPanel) getWidget();
me.add(header = new Grid(1, Cols.values().length));
- me.add(body = new FlowPanel());
+ me.add(body);
headerText = new InlineLabel();
if (titleWidget == null) {
@@ -107,7 +108,7 @@
headerText.setText(text);
header.setVisible(true);
}
- if (windowTitle == null || windowTitle == old) {
+ if (windowTitle == null || windowTitle.equals(old)) {
setWindowTitle(text);
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UserActivityMonitor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UserActivityMonitor.java
new file mode 100644
index 0000000..6e754a2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UserActivityMonitor.java
@@ -0,0 +1,113 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.MouseMoveEvent;
+import com.google.gwt.event.dom.client.MouseMoveHandler;
+import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.event.shared.SimpleEventBus;
+import com.google.gwt.user.client.History;
+import com.google.gwtexpui.globalkey.client.DocWidget;
+
+/** Checks for user keyboard and mouse activity. */
+public class UserActivityMonitor {
+ private static final long TIMEOUT = 10 * 60 * 1000;
+ private static final MonitorImpl impl;
+
+ /**
+ * @return true if there has been keyboard and/or mouse activity in recent
+ * enough history to believe a user is still controlling this session.
+ */
+ public static boolean isActive() {
+ return impl.active || impl.recent;
+ }
+
+ public static HandlerRegistration addValueChangeHandler(
+ ValueChangeHandler<Boolean> handler) {
+ return impl.addValueChangeHandler(handler);
+ }
+
+ static {
+ impl = new MonitorImpl();
+ DocWidget.get().addKeyPressHandler(impl);
+ DocWidget.get().addMouseMoveHandler(impl);
+ History.addValueChangeHandler(impl);
+ Scheduler.get().scheduleFixedDelay(impl, 60 * 1000);
+ }
+
+ private UserActivityMonitor() {
+ }
+
+ private static class MonitorImpl implements RepeatingCommand,
+ KeyPressHandler, MouseMoveHandler, ValueChangeHandler<String>,
+ HasValueChangeHandlers<Boolean> {
+ private final EventBus bus = new SimpleEventBus();
+ private boolean recent = true;
+ private boolean active = true;
+ private long last = System.currentTimeMillis();
+
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ recent = true;
+ }
+
+ @Override
+ public void onMouseMove(MouseMoveEvent event) {
+ recent = true;
+ }
+
+ @Override
+ public void onValueChange(ValueChangeEvent<String> event) {
+ recent = true;
+ }
+
+ @Override
+ public boolean execute() {
+ long now = System.currentTimeMillis();
+ if (recent) {
+ if (!active) {
+ ValueChangeEvent.fire(this, active);
+ }
+ recent = false;
+ active = true;
+ last = now;
+ } else if (active && (now - last) > TIMEOUT) {
+ active = false;
+ ValueChangeEvent.fire(this, false);
+ }
+ return true;
+ }
+
+ @Override
+ public HandlerRegistration addValueChangeHandler(
+ ValueChangeHandler<Boolean> handler) {
+ return bus.addHandler(ValueChangeEvent.getType(), handler);
+ }
+
+ @Override
+ public void fireEvent(GwtEvent<?> event) {
+ bus.fireEvent(event);
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml b/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml
new file mode 100644
index 0000000..24a0f57
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/CodeMirror.gwt.xml
@@ -0,0 +1,23 @@
+<!--
+ Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<module>
+ <inherits name='com.google.gwt.logging.Logging'/>
+ <inherits name='com.google.gwt.resources.Resources'/>
+ <source path='addon'/>
+ <source path='lib'/>
+ <source path='keymap'/>
+ <source path='mode'/>
+</module>
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
new file mode 100644
index 0000000..4db989c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -0,0 +1,347 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import net.codemirror.lib.TextMarker.FromTo;
+
+/**
+ * Glue to connect CodeMirror to be callable from GWT.
+ *
+ * @link http://codemirror.net/doc/manual.html#api
+ */
+public class CodeMirror extends JavaScriptObject {
+ public static void initLibrary(AsyncCallback<Void> cb) {
+ Loader.initLibrary(cb);
+ }
+
+ public static native CodeMirror create(Element parent,
+ Configuration cfg) /*-{
+ return $wnd.CodeMirror(parent, cfg);
+ }-*/;
+
+ public final native void setOption(String option, boolean value) /*-{
+ this.setOption(option, value);
+ }-*/;
+
+ public final native void setOption(String option, double value) /*-{
+ this.setOption(option, value);
+ }-*/;
+
+ public final native void setOption(String option, JavaScriptObject val) /*-{
+ this.setOption(option, val);
+ }-*/;
+
+ public final native void setOptionToInfinity(String option) /*-{
+ this.setOption(option, Infinity);
+ }-*/;
+
+ public final native void setValue(String v) /*-{ this.setValue(v); }-*/;
+
+ public final native void setWidth(double w) /*-{ this.setSize(w, null); }-*/;
+ public final native void setWidth(String w) /*-{ this.setSize(w, null); }-*/;
+ public final native void setHeight(double h) /*-{ this.setSize(null, h); }-*/;
+ public final native void setHeight(String h) /*-{ this.setSize(null, h); }-*/;
+
+ public final native void refresh() /*-{ this.refresh(); }-*/;
+ public final native Element getWrapperElement() /*-{ return this.getWrapperElement(); }-*/;
+
+ public final native TextMarker markText(LineCharacter from, LineCharacter to,
+ Configuration options) /*-{
+ return this.markText(from, to, options);
+ }-*/;
+
+ public enum LineClassWhere {
+ TEXT, BACKGROUND, WRAP;
+ }
+
+ public final void addLineClass(int line, LineClassWhere where,
+ String className) {
+ addLineClassNative(line, where.name().toLowerCase(), className);
+ }
+
+ private final native void addLineClassNative(int line, String where,
+ String lineClass) /*-{
+ this.addLineClass(line, where, lineClass);
+ }-*/;
+
+ public final void addLineClass(LineHandle line, LineClassWhere where,
+ String className) {
+ addLineClassNative(line, where.name().toLowerCase(), className);
+ }
+
+ private final native void addLineClassNative(LineHandle line, String where,
+ String lineClass) /*-{
+ this.addLineClass(line, where, lineClass);
+ }-*/;
+
+ public final void removeLineClass(int line, LineClassWhere where,
+ String className) {
+ removeLineClassNative(line, where.name().toLowerCase(), className);
+ }
+
+ private final native void removeLineClassNative(int line, String where,
+ String lineClass) /*-{
+ this.removeLineClass(line, where, lineClass);
+ }-*/;
+
+ public final void removeLineClass(LineHandle line, LineClassWhere where,
+ String className) {
+ removeLineClassNative(line, where.name().toLowerCase(), className);
+ }
+
+ private final native void removeLineClassNative(LineHandle line, String where,
+ String lineClass) /*-{
+ this.removeLineClass(line, where, lineClass);
+ }-*/;
+
+
+ public final native void addWidget(LineCharacter pos, Element node,
+ boolean scrollIntoView) /*-{
+ this.addWidget(pos, node, scrollIntoView);
+ }-*/;
+
+ public final native LineWidget addLineWidget(int line, Element node,
+ Configuration options) /*-{
+ return this.addLineWidget(line, node, options);
+ }-*/;
+
+ public final native int lineAtHeight(double height) /*-{
+ return this.lineAtHeight(height);
+ }-*/;
+
+ public final native int lineAtHeight(double height, String mode) /*-{
+ return this.lineAtHeight(height, mode);
+ }-*/;
+
+ public final native double heightAtLine(int line) /*-{
+ return this.heightAtLine(line);
+ }-*/;
+
+ public final native double heightAtLine(int line, String mode) /*-{
+ return this.heightAtLine(line, mode);
+ }-*/;
+
+ public final native CodeMirrorDoc getDoc() /*-{
+ return this.getDoc();
+ }-*/;
+
+ public final native void scrollTo(double x, double y) /*-{
+ this.scrollTo(x, y);
+ }-*/;
+
+ public final native void scrollToY(double y) /*-{
+ this.scrollTo(null, y);
+ }-*/;
+
+ public final native ScrollInfo getScrollInfo() /*-{
+ return this.getScrollInfo();
+ }-*/;
+
+ public final native Viewport getViewport() /*-{
+ return this.getViewport();
+ }-*/;
+
+ public final native int getOldViewportSize() /*-{
+ return this.state.oldViewportSize || 0;
+ }-*/;
+
+ public final native void setOldViewportSize(int lines) /*-{
+ this.state.oldViewportSize = lines;
+ }-*/;
+
+ public final native double getScrollSetAt() /*-{
+ return this.state.scrollSetAt || 0;
+ }-*/;
+
+ public final native void setScrollSetAt(double when) /*-{
+ this.state.scrollSetAt = when;
+ }-*/;
+
+ public final native void on(String event, Runnable thunk) /*-{
+ this.on(event, $entry(function() {
+ thunk.@java.lang.Runnable::run()();
+ }));
+ }-*/;
+
+ /** TODO: Break this line after updating GWT */
+ public final native void on(String event, EventHandler handler) /*-{
+ this.on(event, $entry(function(cm, e) {
+ handler.@net.codemirror.lib.CodeMirror.EventHandler::handle(Lnet/codemirror/lib/CodeMirror;Lcom/google/gwt/dom/client/NativeEvent;)(cm, e);
+ }));
+ }-*/;
+
+ public final native void on(String event, RenderLineHandler handler) /*-{
+ this.on(event, $entry(function(cm, h, ele) {
+ handler.@net.codemirror.lib.CodeMirror.RenderLineHandler::handle(Lnet/codemirror/lib/CodeMirror;Lnet/codemirror/lib/CodeMirror$LineHandle;Lcom/google/gwt/dom/client/Element;)(cm, h, ele);
+ }));
+ }-*/;
+
+ public final native void on(String event, GutterClickHandler handler) /*-{
+ this.on(event, $entry(function(cm, l, g, e) {
+ handler.@net.codemirror.lib.CodeMirror.GutterClickHandler::handle(Lnet/codemirror/lib/CodeMirror;ILjava/lang/String;Lcom/google/gwt/dom/client/NativeEvent;)(cm, l, g, e);
+ }));
+ }-*/;
+
+ public final native LineCharacter getCursor() /*-{
+ return this.getCursor();
+ }-*/;
+
+ public final native LineCharacter getCursor(String start) /*-{
+ return this.getCursor(start);
+ }-*/;
+
+ public final FromTo getSelectedRange() {
+ return FromTo.create(getCursor("start"), getCursor("end"));
+ };
+
+ public final native void setCursor(LineCharacter lineCh) /*-{
+ this.setCursor(lineCh);
+ }-*/;
+
+ public final native boolean somethingSelected() /*-{
+ return this.somethingSelected();
+ }-*/;
+
+ public final native boolean hasActiveLine() /*-{
+ return !!this.state.activeLine;
+ }-*/;
+
+ public final native LineHandle getActiveLine() /*-{
+ return this.state.activeLine;
+ }-*/;
+
+ public final native void setActiveLine(LineHandle line) /*-{
+ this.state.activeLine = line;
+ }-*/;
+
+ public final native void addKeyMap(KeyMap map) /*-{ this.addKeyMap(map); }-*/;
+
+ public final native void removeKeyMap(KeyMap map) /*-{ this.removeKeyMap(map); }-*/;
+
+ public final native void removeKeyMap(String name) /*-{ this.removeKeyMap(name); }-*/;
+
+ public static final native LineCharacter pos(int line, int ch) /*-{
+ return $wnd.CodeMirror.Pos(line, ch);
+ }-*/;
+
+ public static final native LineCharacter pos(int line) /*-{
+ return $wnd.CodeMirror.Pos(line);
+ }-*/;
+
+ public final native LineHandle getLineHandle(int line) /*-{
+ return this.getLineHandle(line);
+ }-*/;
+
+ public final native LineHandle getLineHandleVisualStart(int line) /*-{
+ return this.getLineHandleVisualStart(line);
+ }-*/;
+
+ public final native int getLineNumber(LineHandle handle) /*-{
+ return this.getLineNumber(handle);
+ }-*/;
+
+ public final native void focus() /*-{
+ this.focus();
+ }-*/;
+
+ public final native int lineCount() /*-{
+ return this.lineCount();
+ }-*/;
+
+ public final native Element getGutterElement() /*-{
+ return this.getGutterElement();
+ }-*/;
+
+ public final native Element getScrollerElement() /*-{
+ return this.getScrollerElement();
+ }-*/;
+
+ public final native Element getSizer() /*-{
+ return this.display.sizer;
+ }-*/;
+
+ public final native Element getInputField() /*-{
+ return this.getInputField();
+ }-*/;
+
+ public final native Element getScrollbarV() /*-{
+ return this.display.scrollbarV;
+ }-*/;
+
+ public static final native void setObjectProperty(JavaScriptObject obj,
+ String name, boolean value) /*-{
+ obj[name] = value;
+ }-*/;
+
+ public static final native KeyMap cloneKeyMap(String name) /*-{
+ var i = $wnd.CodeMirror.keyMap[name];
+ var o = {};
+ for (n in i)
+ if (i.hasOwnProperty(n))
+ o[n] = i[n];
+ return o;
+ }-*/;
+
+ public static final native void addKeyMap(String name, KeyMap km) /*-{
+ $wnd.CodeMirror.keyMap[name] = km;
+ }-*/;
+
+ public static final native void handleVimKey(CodeMirror cm, String key) /*-{
+ $wnd.CodeMirror.Vim.handleKey(cm, key);
+ }-*/;
+
+ public static final native void mapVimKey(String alias, String actual) /*-{
+ $wnd.CodeMirror.Vim.map(alias, actual);
+ }-*/;
+
+ public final native boolean hasVimSearchHighlight() /*-{
+ return this.state.vim && this.state.vim.searchState_ &&
+ !!this.state.vim.searchState_.getOverlay();
+ }-*/;
+
+ protected CodeMirror() {
+ }
+
+ public static class Viewport extends JavaScriptObject {
+ public final native int getFrom() /*-{ return this.from; }-*/;
+ public final native int getTo() /*-{ return this.to; }-*/;
+
+ protected Viewport() {
+ }
+ }
+
+ public static class LineHandle extends JavaScriptObject {
+ protected LineHandle(){
+ }
+ }
+
+ public interface EventHandler {
+ public void handle(CodeMirror instance, NativeEvent event);
+ }
+
+ public interface RenderLineHandler {
+ public void handle(CodeMirror instance, LineHandle handle, Element element);
+ }
+
+ public interface GutterClickHandler {
+ public void handle(CodeMirror instance, int line, String gutter,
+ NativeEvent clickEvent);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirrorDoc.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirrorDoc.java
new file mode 100644
index 0000000..ff5d230
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirrorDoc.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** The Doc object representing the content in a CodeMirror */
+public class CodeMirrorDoc extends JavaScriptObject {
+
+ public final native void replaceRange(String replacement,
+ LineCharacter from, LineCharacter to) /*-{
+ this.replaceRange(replacement, from, to);
+ }-*/;
+
+ public final native void insertText(String insertion, LineCharacter at) /*-{
+ this.replaceRange(insertion, at);
+ }-*/;
+
+ protected CodeMirrorDoc() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Configuration.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Configuration.java
new file mode 100644
index 0000000..fde7970
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Configuration.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/**
+ * Simple map-like structure to pass configuration to CodeMirror.
+ *
+ * @link http://codemirror.net/doc/manual.html#config
+ * @see CodeMirror#create(com.google.gwt.dom.client.Element, Configuration)
+ */
+public class Configuration extends JavaScriptObject {
+ public static Configuration create() {
+ return createObject().cast();
+ }
+
+ public final native Configuration set(String name, String val)
+ /*-{ this[name] = val; return this; }-*/;
+
+ public final native Configuration set(String name, int val)
+ /*-{ this[name] = val; return this; }-*/;
+
+ public final native Configuration set(String name, boolean val)
+ /*-{ this[name] = val; return this; }-*/;
+
+ public final native Configuration set(String name, JavaScriptObject val)
+ /*-{ this[name] = val; return this; }-*/;
+
+ public final native Configuration setInfinity(String name)
+ /*-{ this[name] = Infinity; return this; }-*/;
+
+ protected Configuration() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java
new file mode 100644
index 0000000..83350c5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/KeyMap.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** Object that associates a key or key combination with a handler. */
+public class KeyMap extends JavaScriptObject {
+ public static KeyMap create() {
+ return createObject().cast();
+ }
+
+ public final native KeyMap on(String key, Runnable thunk) /*-{
+ this[key] = function() { $entry(thunk.@java.lang.Runnable::run()()); };
+ return this;
+ }-*/;
+
+ public final native KeyMap remove(String key) /*-{ delete this[key]; }-*/;
+
+ protected KeyMap() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java
new file mode 100644
index 0000000..bb60fe9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Lib.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.resources.client.DataResource.DoNotEmbed;
+import com.google.gwt.resources.client.ExternalTextResource;
+
+interface Lib extends ClientBundle {
+ static final Lib I = GWT.create(Lib.class);
+
+ @Source("cm3.css")
+ ExternalTextResource css();
+
+ @Source("cm3.js")
+ @DoNotEmbed
+ DataResource js();
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/LineCharacter.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineCharacter.java
new file mode 100644
index 0000000..5076aff
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineCharacter.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** {line, ch} objects used within CodeMirror. */
+public class LineCharacter extends JavaScriptObject {
+ public static LineCharacter create(int line, int ch) {
+ return createImpl(line, ch);
+ }
+
+ public static LineCharacter create(int line) {
+ return createImpl(line, 0);
+ }
+
+ private static LineCharacter createImpl(int line, int ch) {
+ LineCharacter lineCh = createObject().cast();
+ lineCh.setLine(line);
+ lineCh.setCh(ch);
+ return lineCh;
+ }
+
+ public final native void setLine(int line) /*-{ this.line = line; }-*/;
+ public final native void setCh(int ch) /*-{ this.ch = ch; }-*/;
+
+ public final native int getLine() /*-{ return this.line; }-*/;
+ public final native int getCh() /*-{ return this.ch; }-*/;
+
+ protected LineCharacter() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/LineWidget.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineWidget.java
new file mode 100644
index 0000000..c7b0300
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/LineWidget.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** LineWidget objects used within CodeMirror. */
+public class LineWidget extends JavaScriptObject {
+ public static LineWidget create() {
+ return createObject().cast();
+ }
+
+ public final native void clear() /*-{ this.clear(); }-*/;
+ public final native void changed() /*-{ this.changed(); }-*/;
+ public final native JavaScriptObject getLine() /*-{ return this.line; }-*/;
+
+ protected LineWidget() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
new file mode 100644
index 0000000..46e7a71
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gwt.core.client.Callback;
+import com.google.gwt.core.client.ScriptInjector;
+import com.google.gwt.dom.client.ScriptElement;
+import com.google.gwt.dom.client.StyleInjector;
+import com.google.gwt.resources.client.ExternalTextResource;
+import com.google.gwt.resources.client.ResourceCallback;
+import com.google.gwt.resources.client.ResourceException;
+import com.google.gwt.resources.client.TextResource;
+import com.google.gwt.safehtml.shared.SafeUri;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+class Loader {
+ private static native boolean isLibLoaded()
+ /*-{ return $wnd.hasOwnProperty('CodeMirror'); }-*/;
+
+ static void initLibrary(final AsyncCallback<Void> cb) {
+ if (isLibLoaded()) {
+ cb.onSuccess(null);
+ } else {
+ injectCss(Lib.I.css());
+ injectScript(Lib.I.js().getSafeUri(), new GerritCallback<Void>(){
+ @Override
+ public void onSuccess(Void result) {
+ initVimKeys();
+ cb.onSuccess(null);
+ }
+ });
+ }
+ }
+
+ private static void injectCss(ExternalTextResource css) {
+ try {
+ css.getText(new ResourceCallback<TextResource>() {
+ @Override
+ public void onSuccess(TextResource resource) {
+ StyleInjector.inject(resource.getText());
+ }
+
+ @Override
+ public void onError(ResourceException e) {
+ error(e);
+ }
+ });
+ } catch (ResourceException e) {
+ error(e);
+ }
+ }
+
+ static void injectScript(SafeUri js, final AsyncCallback<Void> callback) {
+ final ScriptElement[] script = new ScriptElement[1];
+ script[0] = ScriptInjector.fromUrl(js.asString())
+ .setWindow(ScriptInjector.TOP_WINDOW)
+ .setCallback(new Callback<Void, Exception>() {
+ @Override
+ public void onSuccess(Void result) {
+ script[0].removeFromParent();
+ callback.onSuccess(result);
+ }
+
+ @Override
+ public void onFailure(Exception reason) {
+ error(reason);
+ callback.onFailure(reason);
+ }
+ })
+ .inject()
+ .cast();
+ }
+
+ private static void initVimKeys() {
+ // TODO: Better custom keybindings, remove temporary navigation hacks.
+ KeyMap km = CodeMirror.cloneKeyMap("vim");
+ for (String s : new String[] {"A", "C", "O", "R", "U", "Ctrl-C"}) {
+ km.remove(s);
+ }
+ CodeMirror.addKeyMap("vim_ro", km);
+ }
+
+ private static void error(Exception e) {
+ Logger log = Logger.getLogger("net.codemirror");
+ log.log(Level.SEVERE, "Cannot load portions of CodeMirror", e);
+ }
+
+ private Loader() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
new file mode 100644
index 0000000..4c32f38
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/ModeInjector.java
@@ -0,0 +1,180 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.JsArrayString;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.safehtml.shared.SafeUri;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import net.codemirror.mode.Modes;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class ModeInjector {
+ /** Map of server content type to CodeMiror mode or content type. */
+ private static final Map<String, String> mimeAlias;
+
+ /** Map of content type "text/x-java" to mode name "clike". */
+ private static final Map<String, String> mimeModes;
+
+ /** Map of names such as "clike" to URI for code download. */
+ private static final Map<String, SafeUri> modeUris;
+
+ static {
+ DataResource[] all = {
+ Modes.I.clike(),
+ Modes.I.css(),
+ Modes.I.go(),
+ Modes.I.htmlmixed(),
+ Modes.I.javascript(),
+ Modes.I.properties(),
+ Modes.I.python(),
+ Modes.I.shell(),
+ Modes.I.sql(),
+ Modes.I.velocity(),
+ Modes.I.xml(),
+ };
+
+ mimeAlias = new HashMap<String, String>();
+ mimeModes = new HashMap<String, String>();
+ modeUris = new HashMap<String, SafeUri>();
+
+ for (DataResource m : all) {
+ modeUris.put(m.getName(), m.getSafeUri());
+ }
+ parseModeMap();
+ }
+
+ private static void parseModeMap() {
+ String mode = null;
+ for (String line : Modes.I.mode_map().getText().split("\n")) {
+ int eq = line.indexOf('=');
+ if (0 < eq) {
+ mimeAlias.put(
+ line.substring(0, eq).trim(),
+ line.substring(eq + 1).trim());
+ } else if (line.endsWith(":")) {
+ String n = line.substring(0, line.length() - 1);
+ if (modeUris.containsKey(n)) {
+ mode = n;
+ }
+ } else if (mode != null && line.contains("/")) {
+ mimeModes.put(line, mode);
+ } else {
+ mode = null;
+ }
+ }
+ }
+
+ public static String getContentType(String mode) {
+ String real = mode != null ? mimeAlias.get(mode) : null;
+ return real != null ? real : mode;
+ }
+
+ private static native boolean isModeLoaded(String n)
+ /*-{ return $wnd.CodeMirror.modes.hasOwnProperty(n); }-*/;
+
+ private static native boolean isMimeLoaded(String n)
+ /*-{ return $wnd.CodeMirror.mimeModes.hasOwnProperty(n); }-*/;
+
+ private static native JsArrayString getDependencies(String n)
+ /*-{ return $wnd.CodeMirror.modes[n].dependencies || []; }-*/;
+
+ private final Set<String> loading = new HashSet<String>(4);
+ private int pending;
+ private AsyncCallback<Void> appCallback;
+
+ public ModeInjector add(String name) {
+ if (name == null || isModeLoaded(name) || isMimeLoaded(name)) {
+ return this;
+ }
+
+ String mode = mimeModes.get(name);
+ if (mode == null) {
+ mode = name;
+ }
+
+ SafeUri uri = modeUris.get(mode);
+ if (uri == null) {
+ Logger.getLogger("net.codemirror").log(
+ Level.WARNING,
+ "CodeMirror mode " + mode + " not configured.");
+ return this;
+ }
+
+ loading.add(mode);
+ return this;
+ }
+
+ public void inject(AsyncCallback<Void> appCallback) {
+ this.appCallback = appCallback;
+ for (String mode : loading) {
+ beginLoading(mode);
+ }
+ if (pending == 0) {
+ appCallback.onSuccess(null);
+ }
+ }
+
+ private void beginLoading(final String mode) {
+ pending++;
+ Loader.injectScript(
+ modeUris.get(mode),
+ new AsyncCallback<Void>() {
+ @Override
+ public void onSuccess(Void result) {
+ pending--;
+ ensureDependenciesAreLoaded(mode);
+ if (pending == 0) {
+ appCallback.onSuccess(null);
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ if (--pending == 0) {
+ appCallback.onFailure(caught);
+ }
+ }
+ });
+ }
+
+ private void ensureDependenciesAreLoaded(String mode) {
+ JsArrayString deps = getDependencies(mode);
+ for (int i = 0; i < deps.length(); i++) {
+ String d = deps.get(i);
+ if (loading.contains(d) || isModeLoaded(d)) {
+ continue;
+ }
+
+ SafeUri uri = modeUris.get(d);
+ if (uri == null) {
+ Logger.getLogger("net.codemirror").log(
+ Level.SEVERE,
+ "CodeMirror mode " + mode + " needs " + d);
+ continue;
+ }
+
+ loading.add(d);
+ beginLoading(d);
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/ScrollInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/ScrollInfo.java
new file mode 100644
index 0000000..0639416
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/ScrollInfo.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** {left, top, width, height, clientWidth, clientHeight} objects returned by
+ * getScrollInfo(). */
+public class ScrollInfo extends JavaScriptObject {
+ public static ScrollInfo create() {
+ return createObject().cast();
+ }
+
+ public final native double getLeft() /*-{ return this.left; }-*/;
+ public final native double getTop() /*-{ return this.top; }-*/;
+ public final native double getWidth() /*-{ return this.width; }-*/;
+ public final native double getHeight() /*-{ return this.height; }-*/;
+ public final native double getClientWidth() /*-{ return this.clientWidth; }-*/;
+ public final native double getClientHeight() /*-{ return this.clientHeight; }-*/;
+
+ protected ScrollInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
new file mode 100644
index 0000000..f154e48
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.lib;
+
+import com.google.gerrit.client.diff.CommentRange;
+import com.google.gwt.core.client.JavaScriptObject;
+
+/** Object that represents a text marker within CodeMirror */
+public class TextMarker extends JavaScriptObject {
+ public static TextMarker create() {
+ return createObject().cast();
+ }
+
+ public final native void clear() /*-{ this.clear(); }-*/;
+ public final native void changed() /*-{ this.changed(); }-*/;
+ public final native FromTo find() /*-{ return this.find(); }-*/;
+
+ protected TextMarker() {
+ }
+
+ public static class FromTo extends JavaScriptObject {
+ public static FromTo create(LineCharacter from, LineCharacter to) {
+ FromTo fromTo = createObject().cast();
+ fromTo.setFrom(from);
+ fromTo.setTo(to);
+ return fromTo;
+ }
+
+ public static FromTo create(CommentRange range) {
+ return create(
+ LineCharacter.create(range.start_line() - 1, range.start_character()),
+ LineCharacter.create(range.end_line() - 1, range.end_character()));
+ }
+
+ public final native LineCharacter getFrom() /*-{ return this.from; }-*/;
+ public final native LineCharacter getTo() /*-{ return this.to; }-*/;
+
+ public final native void setFrom(LineCharacter from) /*-{ this.from = from; }-*/;
+ public final native void setTo(LineCharacter to) /*-{ this.to = to; }-*/;
+
+ protected FromTo() {
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
new file mode 100644
index 0000000..e2d3e3c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net.codemirror.mode;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.resources.client.DataResource.DoNotEmbed;
+import com.google.gwt.resources.client.TextResource;
+
+public interface Modes extends ClientBundle {
+ public static final Modes I = GWT.create(Modes.class);
+
+ @Source("mode_map") TextResource mode_map();
+ @Source("clike/clike.js") @DoNotEmbed DataResource clike();
+ @Source("css/css.js") @DoNotEmbed DataResource css();
+ @Source("go/go.js") @DoNotEmbed DataResource go();
+ @Source("htmlmixed/htmlmixed.js") @DoNotEmbed DataResource htmlmixed();
+ @Source("javascript/javascript.js") @DoNotEmbed DataResource javascript();
+ @Source("properties/properties.js") @DoNotEmbed DataResource properties();
+ @Source("python/python.js") @DoNotEmbed DataResource python();
+ @Source("shell/shell.js") @DoNotEmbed DataResource shell();
+ @Source("sql/sql.js") @DoNotEmbed DataResource sql();
+ @Source("velocity/velocity.js") @DoNotEmbed DataResource velocity();
+ @Source("xml/xml.js") @DoNotEmbed DataResource xml();
+
+ // When adding a resource, update static initializer in ModeInjector.
+}
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map b/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
new file mode 100644
index 0000000..e448fa9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/mode_map
@@ -0,0 +1,55 @@
+clike:
+text/x-csrc
+text/x-c
+text/x-chdr
+text/x-c++src
+text/x-c++hdr
+text/x-java
+text/x-csharp
+text/x-scala
+
+css:
+text/css
+text/x-scss
+
+go:
+text/x-go
+
+htmlmixed:
+text/html
+
+javascript:
+text/javascript
+text/ecmascript
+application/javascript
+application/ecmascript
+application/json
+application/x-json
+text/typescript
+application/typescript
+
+properties:
+text/x-ini
+text/x-properties
+
+python:
+text/x-python
+
+shell:
+text/x-sh
+
+sql:
+text/x-sql
+text/x-mariadb
+text/x-mysql
+text/x-plsql
+
+velocity:
+text/velocity
+
+xml:
+text/xml
+application/xml
+
+application/x-javascript = application/javascript
+text/x-java-source = text/x-java
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/EditIteratorTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/EditIteratorTest.java
new file mode 100644
index 0000000..12a616a
--- /dev/null
+++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/EditIteratorTest.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+
+import com.googlecode.gwt.test.GwtModule;
+import com.googlecode.gwt.test.GwtTest;
+
+import net.codemirror.lib.LineCharacter;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/** Unit tests for EditIterator */
+@GwtModule("com.google.gerrit.GerritGwtUI")
+public class EditIteratorTest extends GwtTest {
+ private JsArrayString lines;
+
+ private void assertLineChsEqual(LineCharacter a, LineCharacter b) {
+ assertEquals(a.getLine() + "," + a.getCh(), b.getLine() + "," + b.getCh());
+ }
+
+ @Before
+ public void initialize() {
+ lines = (JsArrayString) JavaScriptObject.createArray();
+ lines.push("1st");
+ lines.push("2nd");
+ lines.push("3rd");
+ }
+
+ @Test
+ public void testNoAdvance() {
+ EditIterator iter = new EditIterator(lines, 0);
+ assertLineChsEqual(LineCharacter.create(0), iter.advance(0));
+ }
+
+ @Test
+ public void testSimpleAdvance() {
+ EditIterator iter = new EditIterator(lines, 0);
+ assertLineChsEqual(LineCharacter.create(0, 1), iter.advance(1));
+ }
+
+ @Test
+ public void testEndsBeforeNewline() {
+ EditIterator iter = new EditIterator(lines, 0);
+ assertLineChsEqual(LineCharacter.create(0, 3), iter.advance(3));
+ }
+
+ @Test
+ public void testEndsOnNewline() {
+ EditIterator iter = new EditIterator(lines, 0);
+ assertLineChsEqual(LineCharacter.create(1), iter.advance(4));
+ }
+
+ @Test
+ public void testAcrossNewline() {
+ EditIterator iter = new EditIterator(lines, 0);
+ assertLineChsEqual(LineCharacter.create(1, 1), iter.advance(5));
+ }
+
+ @Test
+ public void testContinueFromBeforeNewline() {
+ EditIterator iter = new EditIterator(lines, 0);
+ iter.advance(3);
+ assertLineChsEqual(LineCharacter.create(2, 2), iter.advance(7));
+ }
+
+ @Test
+ public void testContinueFromAfterNewline() {
+ EditIterator iter = new EditIterator(lines, 0);
+ iter.advance(4);
+ assertLineChsEqual(LineCharacter.create(2, 2), iter.advance(6));
+ }
+
+ @Test
+ public void testAcrossMultipleLines() {
+ EditIterator iter = new EditIterator(lines, 0);
+ assertLineChsEqual(LineCharacter.create(2, 2), iter.advance(10));
+ }
+}
diff --git a/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java
new file mode 100644
index 0000000..756e879
--- /dev/null
+++ b/gerrit-gwtui/src/test/java/com/google/gerrit/client/diff/LineMapperTest.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.diff;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
+
+import org.junit.Test;
+
+/** Unit tests for LineMapper */
+public class LineMapperTest {
+
+ @Test
+ public void testAppendCommon() {
+ LineMapper mapper = new LineMapper();
+ mapper.appendCommon(10);
+ assertEquals(10, mapper.getLineA());
+ assertEquals(10, mapper.getLineB());
+ }
+
+ @Test
+ public void testAppendInsert() {
+ LineMapper mapper = new LineMapper();
+ mapper.appendInsert(10);
+ assertEquals(0, mapper.getLineA());
+ assertEquals(10, mapper.getLineB());
+ }
+
+ @Test
+ public void testAppendDelete() {
+ LineMapper mapper = new LineMapper();
+ mapper.appendDelete(10);
+ assertEquals(10, mapper.getLineA());
+ assertEquals(0, mapper.getLineB());
+ }
+
+ @Test
+ public void testFindInCommon() {
+ LineMapper mapper = new LineMapper();
+ mapper.appendCommon(10);
+ assertEquals(new LineOnOtherInfo(9, true),
+ mapper.lineOnOther(DisplaySide.A, 9));
+ assertEquals(new LineOnOtherInfo(9, true),
+ mapper.lineOnOther(DisplaySide.B, 9));
+ }
+
+ @Test
+ public void testFindAfterCommon() {
+ LineMapper mapper = new LineMapper();
+ mapper.appendCommon(10);
+ assertEquals(new LineOnOtherInfo(10, true),
+ mapper.lineOnOther(DisplaySide.A, 10));
+ assertEquals(new LineOnOtherInfo(10, true),
+ mapper.lineOnOther(DisplaySide.B, 10));
+ }
+
+ @Test
+ public void testFindInInsertGap() {
+ LineMapper mapper = new LineMapper();
+ mapper.appendInsert(10);
+ assertEquals(new LineOnOtherInfo(-1, false),
+ mapper.lineOnOther(DisplaySide.B, 9));
+ }
+
+ @Test
+ public void testFindAfterInsertGap() {
+ LineMapper mapper = new LineMapper();
+ mapper.appendInsert(10);
+ assertEquals(new LineOnOtherInfo(0, true),
+ mapper.lineOnOther(DisplaySide.B, 10));
+ assertEquals(new LineOnOtherInfo(10, true),
+ mapper.lineOnOther(DisplaySide.A, 0));
+ }
+
+ @Test
+ public void testFindInDeleteGap() {
+ LineMapper mapper = new LineMapper();
+ mapper.appendDelete(10);
+ assertEquals(new LineOnOtherInfo(-1, false),
+ mapper.lineOnOther(DisplaySide.A, 9));
+ }
+
+ @Test
+ public void testFindAfterDeleteGap() {
+ LineMapper mapper = new LineMapper();
+ mapper.appendDelete(10);
+ assertEquals(new LineOnOtherInfo(0, true),
+ mapper.lineOnOther(DisplaySide.A, 10));
+ assertEquals(new LineOnOtherInfo(10, true),
+ mapper.lineOnOther(DisplaySide.B, 0));
+ }
+}
diff --git a/gerrit-gwtui/src/test/resources/META-INF/gwt-test-utils.properties b/gerrit-gwtui/src/test/resources/META-INF/gwt-test-utils.properties
new file mode 100644
index 0000000..c0cbb30
--- /dev/null
+++ b/gerrit-gwtui/src/test/resources/META-INF/gwt-test-utils.properties
@@ -0,0 +1 @@
+com.google.gerrit.GerritGwtUI = gwt-module
diff --git a/gerrit-httpd/BUCK b/gerrit-httpd/BUCK
new file mode 100644
index 0000000..c906459
--- /dev/null
+++ b/gerrit-httpd/BUCK
@@ -0,0 +1,67 @@
+SRCS = glob(['src/main/java/**/*.java'])
+RESOURCES = glob(['src/main/resources/**/*'])
+
+java_library2(
+ name = 'httpd',
+ srcs = SRCS,
+ resources = RESOURCES,
+ deps = [
+ '//gerrit-antlr:query_exception',
+ '//gerrit-common:server',
+ '//gerrit-extension-api:api',
+ '//gerrit-gwtexpui:linker_server',
+ '//gerrit-gwtexpui:server',
+ '//gerrit-patch-jgit:server',
+ '//gerrit-prettify:server',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//gerrit-util-cli:cli',
+ '//lib:args4j',
+ '//lib:gson',
+ '//lib:guava',
+ '//lib:gwtjsonrpc',
+ '//lib:gwtorm',
+ '//lib:jsch',
+ '//lib:jsr305',
+ '//lib:mime-util',
+ '//lib/commons:codec',
+ '//lib/guice:guice',
+ '//lib/guice:guice-assistedinject',
+ '//lib/guice:guice-servlet',
+ '//lib/jgit:jgit',
+ '//lib/jgit:jgit-servlet',
+ '//lib/log:api',
+ ],
+ compile_deps = ['//lib:servlet-api-3_0'],
+ visibility = ['PUBLIC'],
+)
+
+java_sources(
+ name = 'httpd-src',
+ srcs = SRCS + RESOURCES,
+ visibility = ['PUBLIC'],
+)
+
+java_test(
+ name = 'httpd_tests',
+ srcs = glob(['src/test/java/**/*.java']),
+ deps = [
+ ':httpd',
+ '//gerrit-common:server',
+ '//gerrit-extension-api:api',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//lib:easymock',
+ '//lib:junit',
+ '//lib:gson',
+ '//lib:gwtorm',
+ '//lib:guava',
+ '//lib:servlet-api-3_0',
+ '//lib/guice:guice',
+ '//lib/jgit:jgit',
+ '//lib/jgit:junit',
+ ],
+ source_under_test = [':httpd'],
+ # TODO(sop) Remove after Buck supports Eclipse
+ visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
deleted file mode 100644
index e498cac..0000000
--- a/gerrit-httpd/pom.xml
+++ /dev/null
@@ -1,95 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-httpd</artifactId>
- <name>Gerrit Code Review - HTTPd</name>
-
- <description>
- Servlet context for components run inside of an HTTP environment.
- </description>
-
- <dependencies>
- <dependency>
- <groupId>org.apache.tomcat</groupId>
- <artifactId>servlet-api</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.code.findbugs</groupId>
- <artifactId>jsr305</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit.http.server</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit.junit</artifactId>
- </dependency>
-
- <dependency>
- <groupId>eu.medsea.mimeutil</groupId>
- <artifactId>mime-util</artifactId>
- </dependency>
-
- <dependency>
- <groupId>gwtjsonrpc</groupId>
- <artifactId>gwtjsonrpc</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-server</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-prettify</artifactId>
- <version>${project.version}</version>
- </dependency>
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-source-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>jar</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 96792f0..9968e2a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -185,6 +185,7 @@
public void setUserAccountId(Account.Id id) {
key = new Key("id:" + id);
val = new Val(id, 0, false, null, 0, null, null);
+ user = identified.runAs(id, user);
}
@Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
index 7241624..677a615 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
@@ -21,6 +21,7 @@
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.DownloadConfig;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.contact.ContactStore;
@@ -35,6 +36,7 @@
import java.net.MalformedURLException;
import java.util.HashSet;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import javax.servlet.ServletContext;
@@ -92,14 +94,19 @@
config.setHttpPasswordUrl(cfg.getString("auth", null, "httpPasswordUrl"));
break;
- case CLIENT_SSL_CERT_LDAP:
- case DEVELOPMENT_BECOME_ANY_ACCOUNT:
case HTTP:
case HTTP_LDAP:
+ config.setLoginUrl(cfg.getString("auth", null, "loginurl"));
+ config.setLoginText(cfg.getString("auth", null, "logintext"));
+ break;
+
+ case CLIENT_SSL_CERT_LDAP:
+ case DEVELOPMENT_BECOME_ANY_ACCOUNT:
case OPENID:
case OPENID_SSO:
break;
}
+ config.setSwitchAccountUrl(cfg.getString("auth", null, "switchAccountUrl"));
config.setUseContributorAgreements(cfg.getBoolean("auth",
"contributoragreements", false));
config.setGitDaemonUrl(cfg.getString("gerrit", null, "canonicalgiturl"));
@@ -115,6 +122,8 @@
"test", false));
config.setAnonymousCowardName(anonymousCowardName);
config.setSuggestFrom(cfg.getInt("suggest", "from", 0));
+ config.setChangeUpdateDelay((int) ConfigUtil.getTimeUnit(
+ cfg, "change", null, "updateDelay", 30, TimeUnit.SECONDS));
config.setReportBugUrl(cfg.getString("gerrit", null, "reportBugUrl"));
if (config.getReportBugUrl() == null) {
@@ -123,13 +132,16 @@
config.setReportBugUrl(null);
}
+ config.setGitBasicAuth(authConfig.isGitBasichAuth());
+
final Set<Account.FieldName> fields = new HashSet<Account.FieldName>();
for (final Account.FieldName n : Account.FieldName.values()) {
if (realm.allowsEdit(n)) {
fields.add(n);
}
}
- if (emailSender != null && emailSender.isEnabled()) {
+ if (emailSender != null && emailSender.isEnabled()
+ && realm.allowsEdit(Account.FieldName.REGISTER_NEW_EMAIL)) {
fields.add(Account.FieldName.REGISTER_NEW_EMAIL);
}
config.setEditableAccountFields(fields);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index dad9b80..4ecd020 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -256,7 +256,7 @@
throws ServiceNotAuthorizedException {
final ProjectControl pc = (ProjectControl) req.getAttribute(ATT_CONTROL);
- if (!(pc.getCurrentUser() instanceof IdentifiedUser)) {
+ if (!(pc.getCurrentUser().isIdentifiedUser())) {
// Anonymous users are not permitted to push.
throw new ServiceNotAuthorizedException();
}
@@ -309,14 +309,11 @@
}
if (!rp.isCheckReferencedObjectsAreReachable()) {
- if (isGet) {
- rc.advertiseHistory();
- }
chain.doFilter(request, response);
return;
}
- if (!(pc.getCurrentUser() instanceof IdentifiedUser)) {
+ if (!(pc.getCurrentUser().isIdentifiedUser())) {
chain.doFilter(request, response);
return;
}
@@ -326,7 +323,6 @@
projectName);
if (isGet) {
- rc.advertiseHistory();
cache.invalidate(cacheKey);
} else {
Set<ObjectId> ids = cache.getIfPresent(cacheKey);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index 3d9f4c8..d87e4af 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -189,6 +189,7 @@
}
@Override
+ @Deprecated
public void setStatus(int sc, String sm) {
status(sc);
super.setStatus(sc, sm);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
index c38425d..f1bddeb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
@@ -332,6 +332,7 @@
}
@Override
+ @Deprecated
public void setStatus(int sc, String sm) {
status(sc);
super.setStatus(sc, sm);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
index 499c2a5..2448d3f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
@@ -15,7 +15,6 @@
package com.google.gerrit.httpd;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -52,7 +51,7 @@
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
throws IOException, ServletException {
- if (user.get() instanceof IdentifiedUser) {
+ if (user.get().isIdentifiedUser()) {
chain.doFilter(request, response);
} else {
HttpServletResponse res = (HttpServletResponse) response;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
new file mode 100644
index 0000000..e0bef35
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -0,0 +1,123 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd;
+
+import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
+import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.servlet.ServletModule;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Allows running a request as another user account. */
+@Singleton
+class RunAsFilter implements Filter {
+ private static final Logger log = LoggerFactory.getLogger(RunAsFilter.class);
+ private static final String RUN_AS = "X-Gerrit-RunAs";
+
+ static class Module extends ServletModule {
+ @Override
+ protected void configureServlets() {
+ filter("/*").through(RunAsFilter.class);
+ }
+ }
+
+ private final boolean enabled;
+ private final Provider<WebSession> session;
+ private final AccountResolver accountResolver;
+
+ @Inject
+ RunAsFilter(AuthConfig config,
+ Provider<WebSession> session,
+ AccountResolver accountResolver) {
+ this.enabled = config.isRunAsEnabled();
+ this.session = session;
+ this.accountResolver = accountResolver;
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest req = (HttpServletRequest) request;
+ HttpServletResponse res = (HttpServletResponse) response;
+
+ String runas = req.getHeader(RUN_AS);
+ if (runas != null) {
+ if (!enabled) {
+ RestApiServlet.replyError(req, res,
+ SC_FORBIDDEN,
+ RUN_AS + " disabled by auth.enableRunAs = false");
+ return;
+ }
+
+ CurrentUser self = session.get().getCurrentUser();
+ if (!self.getCapabilities().canRunAs()) {
+ RestApiServlet.replyError(req, res,
+ SC_FORBIDDEN,
+ "not permitted to use " + RUN_AS);
+ return;
+ }
+
+ Account target;
+ try {
+ target = accountResolver.find(runas);
+ } catch (OrmException e) {
+ log.warn("cannot resolve account for " + RUN_AS, e);
+ RestApiServlet.replyError(req, res,
+ SC_INTERNAL_SERVER_ERROR,
+ "cannot resolve " + RUN_AS);
+ return;
+ }
+ if (target == null) {
+ RestApiServlet.replyError(req, res,
+ SC_FORBIDDEN,
+ "no account matches " + RUN_AS);
+ return;
+ }
+ session.get().setUserAccountId(target.getId());
+ }
+
+ chain.doFilter(req, res);
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) {
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index bd25faf..bf39bfb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -21,12 +21,15 @@
import com.google.gerrit.httpd.raw.CatServlet;
import com.google.gerrit.httpd.raw.HostPageServlet;
import com.google.gerrit.httpd.raw.LegacyGerritServlet;
+import com.google.gerrit.httpd.raw.RobotsServlet;
import com.google.gerrit.httpd.raw.SshInfoServlet;
import com.google.gerrit.httpd.raw.StaticServlet;
import com.google.gerrit.httpd.raw.ToolServlet;
+import com.google.gerrit.httpd.rpc.access.AccessRestApiServlet;
import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
import com.google.gerrit.httpd.rpc.change.ChangesRestApiServlet;
import com.google.gerrit.httpd.rpc.change.DeprecatedChangeQueryServlet;
+import com.google.gerrit.httpd.rpc.config.ConfigRestApiServlet;
import com.google.gerrit.httpd.rpc.group.GroupsRestApiServlet;
import com.google.gerrit.httpd.rpc.project.ProjectsRestApiServlet;
import com.google.gerrit.reviewdb.client.Change;
@@ -100,14 +103,18 @@
filter("/a/*").through(RequireIdentifiedUserFilter.class);
serveRegex("^/(?:a/)?tools/(.*)$").with(ToolServlet.class);
+ serveRegex("^/(?:a/)?access/(.*)$").with(AccessRestApiServlet.class);
serveRegex("^/(?:a/)?accounts/(.*)$").with(AccountsRestApiServlet.class);
serveRegex("^/(?:a/)?changes/(.*)$").with(ChangesRestApiServlet.class);
+ serveRegex("^/(?:a/)?config/(.*)$").with(ConfigRestApiServlet.class);
serveRegex("^/(?:a/)?groups/(.*)?$").with(GroupsRestApiServlet.class);
serveRegex("^/(?:a/)?projects/(.*)?$").with(ProjectsRestApiServlet.class);
if (cfg.deprecatedQuery) {
serve("/query").with(DeprecatedChangeQueryServlet.class);
}
+
+ serve("/robots.txt").with(RobotsServlet.class);
}
private Key<HttpServlet> notFound() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index f7ba6df..4d97cc2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -27,10 +27,7 @@
import com.google.gerrit.httpd.gitweb.GitWebModule;
import com.google.gerrit.httpd.rpc.UiRpcModule;
import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.CmdLineParserModule;
import com.google.gerrit.server.RemotePeer;
-import com.google.gerrit.server.account.ClearPassword;
-import com.google.gerrit.server.account.GeneratePassword;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.FactoryModule;
@@ -85,11 +82,12 @@
if (wantSSL) {
install(new RequireSslFilter.Module());
}
+ install(new RunAsFilter.Module());
switch (authConfig.getAuthType()) {
case HTTP:
case HTTP_LDAP:
- install(new HttpAuthModule());
+ install(new HttpAuthModule(authConfig));
break;
case CLIENT_SSL_CERT_LDAP:
@@ -130,10 +128,7 @@
bind(GerritConfig.class).toProvider(GerritConfigProvider.class);
DynamicSet.setOf(binder(), WebUiPlugin.class);
- factory(ClearPassword.Factory.class);
install(new AsyncReceiveCommits.Module());
- install(new CmdLineParserModule());
- factory(GeneratePassword.Factory.class);
bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
HttpRemotePeerProvider.class).in(RequestScoped.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index adca95e..75a7e07 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -61,6 +61,8 @@
private final byte[] signInRaw;
private final byte[] signInGzip;
private final String loginHeader;
+ private final String displaynameHeader;
+ private final String emailHeader;
@Inject
HttpAuthFilter(final Provider<WebSession> webSession,
@@ -78,6 +80,8 @@
loginHeader = firstNonNull(
emptyToNull(authConfig.getLoginHttpHeader()),
AUTHORIZATION);
+ displaynameHeader = emptyToNull(authConfig.getHttpDisplaynameHeader());
+ emailHeader = emptyToNull(authConfig.getHttpEmailHeader());
}
@Override
@@ -174,6 +178,22 @@
}
}
+ String getRemoteDisplayname(HttpServletRequest req) {
+ if (displaynameHeader != null) {
+ return emptyToNull(req.getHeader(displaynameHeader));
+ } else {
+ return null;
+ }
+ }
+
+ String getRemoteEmail(HttpServletRequest req) {
+ if (emailHeader != null) {
+ return emptyToNull(req.getHeader(emailHeader));
+ } else {
+ return null;
+ }
+ }
+
String getLoginHeader() {
return loginHeader;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java
index daaa7e2..638d527 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java
@@ -14,13 +14,22 @@
package com.google.gerrit.httpd.auth.container;
+import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.servlet.ServletModule;
/** Servlets and support related to HTTP authentication. */
public class HttpAuthModule extends ServletModule {
+ private final AuthConfig authConfig;
+
+ public HttpAuthModule(final AuthConfig authConfig) {
+ this.authConfig = authConfig;
+ }
+
@Override
protected void configureServlets() {
- filter("/").through(HttpAuthFilter.class);
+ if (authConfig.getLoginUrl() == null) {
+ filter("/").through(HttpAuthFilter.class);
+ }
serve("/login", "/login/*").with(HttpLoginServlet.class);
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index fe7aa23..e62fde5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -22,6 +22,7 @@
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -36,7 +37,6 @@
import java.io.IOException;
-import javax.annotation.Nullable;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
@@ -60,16 +60,19 @@
private final CanonicalWebUrl urlProvider;
private final AccountManager accountManager;
private final HttpAuthFilter authFilter;
+ private final AuthConfig authConfig;
@Inject
HttpLoginServlet(final Provider<WebSession> webSession,
final CanonicalWebUrl urlProvider,
final AccountManager accountManager,
- final HttpAuthFilter authFilter) {
+ final HttpAuthFilter authFilter,
+ final AuthConfig authConfig) {
this.webSession = webSession;
this.urlProvider = urlProvider;
this.accountManager = accountManager;
this.authFilter = authFilter;
+ this.authConfig = authConfig;
}
@Override
@@ -111,6 +114,8 @@
}
final AuthRequest areq = AuthRequest.forUser(user);
+ areq.setDisplayName(authFilter.getRemoteDisplayname(req));
+ areq.setEmailAddress(authFilter.getRemoteEmail(req));
final AuthResult arsp;
try {
arsp = accountManager.authenticate(areq);
@@ -121,12 +126,16 @@
}
final StringBuilder rdr = new StringBuilder();
- rdr.append(urlProvider.get(req));
- rdr.append('#');
- if (arsp.isNew() && !token.startsWith(PageLinks.REGISTER + "/")) {
- rdr.append(PageLinks.REGISTER);
+ if (arsp.isNew() && authConfig.getRegisterPageUrl() != null) {
+ rdr.append(authConfig.getRegisterPageUrl());
+ } else {
+ rdr.append(urlProvider.get(req));
+ rdr.append('#');
+ if (arsp.isNew() && !token.startsWith(PageLinks.REGISTER + "/")) {
+ rdr.append(PageLinks.REGISTER);
+ }
+ rdr.append(token);
}
- rdr.append(token);
webSession.get().login(arsp, true /* persistent cookie */);
rsp.sendRedirect(rdr.toString());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
index 9a2a7da..037c7bb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
@@ -27,7 +27,6 @@
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.auth.AuthenticationUnavailableException;
-import com.google.gerrit.server.config.SitePaths;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.inject.Inject;
import com.google.inject.Provider;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
index 3be0cd3..0769865 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
@@ -518,7 +518,7 @@
}
String remoteUser = null;
- if (project.getCurrentUser() instanceof IdentifiedUser) {
+ if (project.getCurrentUser().isIdentifiedUser()) {
final IdentifiedUser u = (IdentifiedUser) project.getCurrentUser();
final String user = u.getUserName();
env.set("GERRIT_USER_NAME", user);
@@ -619,7 +619,6 @@
}, "GitWeb-ErrorLogger").start();
}
- @SuppressWarnings("unchecked")
private static Enumeration<String> enumerateHeaderNames(HttpServletRequest req) {
return req.getHeaderNames();
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 549c239..53abc4c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -44,8 +44,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
@@ -63,6 +65,7 @@
import javax.servlet.FilterChain;
import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -172,7 +175,7 @@
}
try {
- WrappedContext ctx = new WrappedContext(plugin, base + name);
+ ServletContext ctx = PluginServletContext.create(plugin, base + name);
filter.init(new WrappedFilterConfig(ctx));
} catch (ServletException e) {
log.warn(String.format("Plugin %s failed to initialize HTTP", name), e);
@@ -299,11 +302,39 @@
}
}
+ private void appendEntriesSection(JarFile jar, List<JarEntry> entries,
+ String sectionTitle, StringBuilder md, String prefix,
+ int nameOffset) throws IOException {
+ if (!entries.isEmpty()) {
+ md.append("## " + sectionTitle + " ##\n");
+ for(JarEntry entry : entries) {
+ String rsrc = entry.getName().substring(prefix.length());
+ String entryTitle;
+ if (rsrc.endsWith(".html")) {
+ entryTitle = rsrc.substring(nameOffset, rsrc.length() - 5).replace('-', ' ');
+ } else if (rsrc.endsWith(".md")) {
+ entryTitle = extractTitleFromMarkdown(jar, entry);
+ if (Strings.isNullOrEmpty(entryTitle)) {
+ entryTitle = rsrc.substring(nameOffset, rsrc.length() - 3).replace('-', ' ');
+ }
+ rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
+ } else {
+ entryTitle = rsrc.substring(nameOffset).replace('-', ' ');
+ }
+ md.append(String.format("* [%s](%s)\n", entryTitle, rsrc));
+ }
+ md.append("\n");
+ }
+ }
+
private void sendAutoIndex(JarFile jar,
String prefix, String pluginName,
ResourceKey cacheKey, HttpServletResponse res) throws IOException {
List<JarEntry> cmds = Lists.newArrayList();
+ List<JarEntry> servlets = Lists.newArrayList();
+ List<JarEntry> restApis = Lists.newArrayList();
List<JarEntry> docs = Lists.newArrayList();
+ JarEntry about = null;
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
@@ -313,8 +344,17 @@
&& (name.endsWith(".md")
|| name.endsWith(".html"))
&& 0 < size && size <= SMALL_RESOURCE) {
- if (name.substring(prefix.length()).startsWith("cmd-")) {
+ name = name.substring(prefix.length());
+ if (name.startsWith("cmd-")) {
cmds.add(entry);
+ } else if (name.startsWith("servlet-")) {
+ servlets.add(entry);
+ } else if (name.startsWith("rest-api-")) {
+ restApis.add(entry);
+ } else if (name.startsWith("about.")) {
+ if (about == null) {
+ about = entry;
+ }
} else {
docs.add(entry);
}
@@ -338,47 +378,32 @@
md.append("\n");
appendPluginInfoTable(md, jar.getManifest().getMainAttributes());
- if (!docs.isEmpty()) {
- md.append("## Documentation ##\n");
- for(JarEntry entry : docs) {
- String rsrc = entry.getName().substring(prefix.length());
- String title;
- if (rsrc.endsWith(".html")) {
- title = rsrc.substring(0, rsrc.length() - 5).replace('-', ' ');
- } else if (rsrc.endsWith(".md")) {
- title = extractTitleFromMarkdown(jar, entry);
- if (Strings.isNullOrEmpty(title)) {
- title = rsrc.substring(0, rsrc.length() - 3).replace('-', ' ');
- }
- rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
+ if (about != null) {
+ InputStreamReader isr = new InputStreamReader(jar.getInputStream(about));
+ BufferedReader reader = new BufferedReader(isr);
+ StringBuilder aboutContent = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (line.isEmpty()) {
+ aboutContent.append("\n");
} else {
- title = rsrc.replace('-', ' ');
+ aboutContent.append(line).append("\n");
}
- md.append(String.format("* [%s](%s)\n", title, rsrc));
}
- md.append("\n");
+ reader.close();
+
+ // Only append the About section if there was anything in it
+ if (aboutContent.toString().trim().length() > 0) {
+ md.append("## About ##\n");
+ md.append("\n").append(aboutContent);
+ }
}
- if (!cmds.isEmpty()) {
- md.append("## Commands ##\n");
- for(JarEntry entry : cmds) {
- String rsrc = entry.getName().substring(prefix.length());
- String title;
- if (rsrc.endsWith(".html")) {
- title = rsrc.substring(4, rsrc.length() - 5).replace('-', ' ');
- } else if (rsrc.endsWith(".md")) {
- title = extractTitleFromMarkdown(jar, entry);
- if (Strings.isNullOrEmpty(title)) {
- title = rsrc.substring(4, rsrc.length() - 3).replace('-', ' ');
- }
- rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
- } else {
- title = rsrc.substring(4).replace('-', ' ');
- }
- md.append(String.format("* [%s](%s)\n", title, rsrc));
- }
- md.append("\n");
- }
+ appendEntriesSection(jar, docs, "Documentation", md, prefix, 0);
+ appendEntriesSection(jar, servlets, "Servlets", md, prefix, "servlet-".length());
+ appendEntriesSection(jar, restApis, "REST APIs", md, prefix, "rest-api-".length());
+ appendEntriesSection(jar, cmds, "Commands", md, prefix, "cmd-".length());
sendMarkdownAsHtml(md.toString(), pluginName, cacheKey, res);
}
@@ -623,8 +648,16 @@
@Override
public String getServletPath() {
- return ((HttpServletRequest) getRequest()).getRequestURI().substring(
- contextPath.length());
+ return getRequestURI().substring(contextPath.length());
+ }
+
+ @Override
+ public String getRequestURI() {
+ String uri = super.getRequestURI();
+ if (uri.startsWith("/a/")) {
+ uri = uri.substring(2);
+ }
+ return uri;
}
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
new file mode 100644
index 0000000..c3395b5
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/PluginServletContext.java
@@ -0,0 +1,241 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.plugins;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.server.plugins.Plugin;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.InputStream;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.Servlet;
+import javax.servlet.ServletContext;
+
+class PluginServletContext {
+ private static final Logger log = LoggerFactory.getLogger("plugin");
+
+ static ServletContext create(Plugin plugin, String contextPath) {
+ return (ServletContext) Proxy.newProxyInstance(
+ PluginServletContext.class.getClassLoader(),
+ new Class[] {ServletContext.class, API.class},
+ new Handler(plugin, contextPath));
+ }
+
+ private PluginServletContext() {
+ }
+
+ private static class Handler implements InvocationHandler, API {
+ private final Plugin plugin;
+ private final String contextPath;
+ private final ConcurrentMap<String, Object> attributes;
+
+ Handler(Plugin plugin, String contextPath) {
+ this.plugin = plugin;
+ this.contextPath = contextPath;
+ this.attributes = Maps.newConcurrentMap();
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+ Method handler;
+ try {
+ handler = API.class.getDeclaredMethod(
+ method.getName(),
+ method.getParameterTypes());
+ } catch (NoSuchMethodException e) {
+ throw new NoSuchMethodError(String.format(
+ "%s does not implement%s",
+ PluginServletContext.class,
+ method.toGenericString()));
+ }
+ return handler.invoke(this, args);
+ }
+
+ @Override
+ public String getContextPath() {
+ return contextPath;
+ }
+
+ @Override
+ public String getInitParameter(String name) {
+ return null;
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Enumeration getInitParameterNames() {
+ return Collections.enumeration(Collections.emptyList());
+ }
+
+ @Override
+ public ServletContext getContext(String name) {
+ return null;
+ }
+
+ @Override
+ public RequestDispatcher getNamedDispatcher(String name) {
+ return null;
+ }
+
+ @Override
+ public RequestDispatcher getRequestDispatcher(String name) {
+ return null;
+ }
+
+ @Override
+ public URL getResource(String name) {
+ return null;
+ }
+
+ @Override
+ public InputStream getResourceAsStream(String name) {
+ return null;
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Set getResourcePaths(String name) {
+ return null;
+ }
+
+ @Override
+ public Servlet getServlet(String name) {
+ return null;
+ }
+
+ @Override
+ public String getRealPath(String name) {
+ return null;
+ }
+
+ @Override
+ public String getServletContextName() {
+ return plugin.getName();
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Enumeration getServletNames() {
+ return Collections.enumeration(Collections.emptyList());
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Enumeration getServlets() {
+ return Collections.enumeration(Collections.emptyList());
+ }
+
+ @Override
+ public void log(Exception reason, String msg) {
+ log(msg, reason);
+ }
+
+ @Override
+ public void log(String msg) {
+ log(msg, null);
+ }
+
+ @Override
+ public void log(String msg, Throwable reason) {
+ log.warn(String.format("[plugin %s] %s", plugin.getName(), msg), reason);
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ return attributes.get(name);
+ }
+
+ @Override
+ public Enumeration<String> getAttributeNames() {
+ return Collections.enumeration(attributes.keySet());
+ }
+
+ @Override
+ public void setAttribute(String name, Object value) {
+ attributes.put(name, value);
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ attributes.remove(name);
+ }
+
+ @Override
+ public String getMimeType(String file) {
+ return null;
+ }
+
+ @Override
+ public int getMajorVersion() {
+ return 2;
+ }
+
+ @Override
+ public int getMinorVersion() {
+ return 5;
+ }
+
+ @Override
+ public String getServerInfo() {
+ String v = Version.getVersion();
+ return "Gerrit Code Review/" + (v != null ? v : "dev");
+ }
+ }
+
+ static interface API {
+ String getContextPath();
+ String getInitParameter(String name);
+ @SuppressWarnings("rawtypes")
+ Enumeration getInitParameterNames();
+ ServletContext getContext(String name);
+ RequestDispatcher getNamedDispatcher(String name);
+ RequestDispatcher getRequestDispatcher(String name);
+ URL getResource(String name);
+ InputStream getResourceAsStream(String name);
+ @SuppressWarnings("rawtypes")
+ Set getResourcePaths(String name);
+ Servlet getServlet(String name);
+ String getRealPath(String name);
+ String getServletContextName();
+ @SuppressWarnings("rawtypes")
+ Enumeration getServletNames();
+ @SuppressWarnings("rawtypes")
+ Enumeration getServlets();
+ void log(Exception reason, String msg);
+ void log(String msg);
+ void log(String msg, Throwable reason);
+ Object getAttribute(String name);
+ Enumeration<String> getAttributeNames();
+ void setAttribute(String name, Object value);
+ void removeAttribute(String name);
+ String getMimeType(String file);
+ int getMajorVersion();
+ int getMinorVersion();
+ String getServerInfo();
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java
deleted file mode 100644
index daeb6ff..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java
+++ /dev/null
@@ -1,178 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.plugins;
-
-import com.google.common.collect.Maps;
-import com.google.gerrit.common.Version;
-import com.google.gerrit.server.plugins.Plugin;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.Set;
-import java.util.concurrent.ConcurrentMap;
-
-import javax.servlet.RequestDispatcher;
-import javax.servlet.Servlet;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-
-class WrappedContext implements ServletContext {
- private static final Logger log = LoggerFactory.getLogger("plugin");
- private final Plugin plugin;
- private final String contextPath;
- private final ConcurrentMap<String, Object> attributes;
-
- WrappedContext(Plugin plugin, String contextPath) {
- this.plugin = plugin;
- this.contextPath = contextPath;
- this.attributes = Maps.newConcurrentMap();
- }
-
- @Override
- public String getContextPath() {
- return contextPath;
- }
-
- @Override
- public String getInitParameter(String name) {
- return null;
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- public Enumeration getInitParameterNames() {
- return Collections.enumeration(Collections.emptyList());
- }
-
- @Override
- public ServletContext getContext(String name) {
- return null;
- }
-
- @Override
- public RequestDispatcher getNamedDispatcher(String name) {
- return null;
- }
-
- @Override
- public RequestDispatcher getRequestDispatcher(String name) {
- return null;
- }
-
- @Override
- public URL getResource(String name) throws MalformedURLException {
- return null;
- }
-
- @Override
- public InputStream getResourceAsStream(String name) {
- return null;
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- public Set getResourcePaths(String name) {
- return null;
- }
-
- @Override
- public Servlet getServlet(String name) throws ServletException {
- return null;
- }
-
- @Override
- public String getRealPath(String name) {
- return null;
- }
-
- @Override
- public String getServletContextName() {
- return plugin.getName();
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- public Enumeration getServletNames() {
- return Collections.enumeration(Collections.emptyList());
- }
-
- @SuppressWarnings("rawtypes")
- @Override
- public Enumeration getServlets() {
- return Collections.enumeration(Collections.emptyList());
- }
-
- @Override
- public void log(Exception reason, String msg) {
- log(msg, reason);
- }
-
- @Override
- public void log(String msg) {
- log(msg, null);
- }
-
- @Override
- public void log(String msg, Throwable reason) {
- log.warn(String.format("[plugin %s] %s", plugin.getName(), msg), reason);
- }
-
- @Override
- public Object getAttribute(String name) {
- return attributes.get(name);
- }
-
- @Override
- public Enumeration<String> getAttributeNames() {
- return Collections.enumeration(attributes.keySet());
- }
-
- @Override
- public void setAttribute(String name, Object value) {
- attributes.put(name, value);
- }
-
- @Override
- public void removeAttribute(String name) {
- attributes.remove(name);
- }
-
- @Override
- public String getMimeType(String file) {
- return null;
- }
-
- @Override
- public int getMajorVersion() {
- return 2;
- }
-
- @Override
- public int getMinorVersion() {
- return 5;
- }
-
- @Override
- public String getServerInfo() {
- String v = Version.getVersion();
- return "Gerrit Code Review/" + (v != null ? v : "dev");
- }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java
index c9107dc..04e49c9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java
@@ -23,9 +23,9 @@
import javax.servlet.ServletContext;
class WrappedFilterConfig implements FilterConfig {
- private final WrappedContext context;
+ private final ServletContext context;
- WrappedFilterConfig(WrappedContext context) {
+ WrappedFilterConfig(ServletContext context) {
this.context = context;
}
@@ -39,7 +39,7 @@
return null;
}
- @SuppressWarnings("rawtypes")
+ @SuppressWarnings({"rawtypes", "unchecked"})
@Override
public Enumeration getInitParameterNames() {
return Collections.enumeration(Collections.emptyList());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index ea2168a..508c41a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -14,10 +14,11 @@
package com.google.gerrit.httpd.raw;
+import com.google.common.collect.Lists;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
-import com.google.common.collect.Lists;
import com.google.common.primitives.Bytes;
+import com.google.gerrit.common.Version;
import com.google.gerrit.common.data.GerritConfig;
import com.google.gerrit.common.data.HostPageData;
import com.google.gerrit.extensions.registration.DynamicSet;
@@ -42,6 +43,7 @@
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
+import org.w3c.dom.Node;
import java.io.File;
import java.io.FileNotFoundException;
@@ -79,6 +81,7 @@
private final String noCacheName;
private final PermutationSelector selector;
private final boolean refreshHeaderFooter;
+ private final StaticServlet staticServlet;
private volatile Page page;
@Inject
@@ -86,7 +89,8 @@
final SitePaths sp, final ThemeFactory themeFactory,
final GerritConfig gc, final ServletContext servletContext,
final DynamicSet<WebUiPlugin> webUiPlugins,
- @GerritServerConfig final Config cfg)
+ @GerritServerConfig final Config cfg,
+ final StaticServlet ss)
throws IOException, ServletException {
currentUser = cu;
session = w;
@@ -96,6 +100,7 @@
signedInTheme = themeFactory.getSignedInTheme();
site = sp;
refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
+ staticServlet = ss;
boolean checkUserAgent = cfg.getBoolean("site", "checkUserAgent", true);
final String pageName = "HostPage.html";
@@ -174,7 +179,7 @@
final Page.Content page = select(req);
final StringWriter w = new StringWriter();
final CurrentUser user = currentUser.get();
- if (user instanceof IdentifiedUser) {
+ if (user.isIdentifiedUser()) {
w.write(HPD_ID + ".account=");
json(((IdentifiedUser) user).getAccount(), w);
w.write(";");
@@ -246,6 +251,26 @@
return pg.get(selector.select(req));
}
+ private void insertETags(Element e) {
+ if ("img".equalsIgnoreCase(e.getTagName())
+ || "script".equalsIgnoreCase(e.getTagName())) {
+ String src = e.getAttribute("src");
+ if (src != null && src.startsWith("static/")) {
+ String name = src.substring("static/".length());
+ StaticServlet.Resource r = staticServlet.getResource(name);
+ if (r != null) {
+ e.setAttribute("src", src + "?e=" + r.etag);
+ }
+ }
+ }
+
+ for (Node n = e.getFirstChild(); n != null; n = n.getNextSibling()) {
+ if (n instanceof Element) {
+ insertETags((Element) n);
+ }
+ }
+ }
+
private static class FileInfo {
private final File path;
private final long time;
@@ -275,6 +300,7 @@
footer = injectXmlFile(hostDoc, "gerrit_footer", site.site_footer);
final HostPageData pageData = new HostPageData();
+ pageData.version = Version.getVersion();
pageData.config = config;
final StringWriter w = new StringWriter();
@@ -376,7 +402,8 @@
return info;
}
- final Element content = html.getDocumentElement();
+ Element content = html.getDocumentElement();
+ insertETags(content);
banner.appendChild(hostDoc.importNode(content, true));
return info;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RobotsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RobotsServlet.java
new file mode 100644
index 0000000..d19a0ce
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RobotsServlet.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.raw;
+
+import com.google.common.io.ByteStreams;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * This class provides a mechanism to use a configurable robots.txt file,
+ * outside of the .war of the application. In order to configure it add the
+ * following to the <code>httpd</code> section of the <code>gerrit.conf</code>
+ * file:
+ *
+ * <pre>
+ * [httpd]
+ * robotsFile = etc/myrobots.txt
+ * </pre>
+ *
+ * If the specified file name is relative it will resolved as a sub directory of
+ * the site directory, if it is absolute it will be used as is.
+ *
+ * If the specified file doesn't exist or isn't readable the servlet will
+ * default to the <code>robots.txt</code> file bundled with the .war file of the
+ * application.
+ */
+@SuppressWarnings("serial")
+@Singleton
+public class RobotsServlet extends HttpServlet {
+ private static final Logger log =
+ LoggerFactory.getLogger(RobotsServlet.class);
+
+ private final File robotsFile;
+
+ @Inject
+ RobotsServlet(@GerritServerConfig final Config config, final SitePaths sitePaths) {
+ File file = sitePaths.resolve(
+ config.getString("httpd", null, "robotsFile"));
+ if (file != null && (!file.exists() || !file.canRead())) {
+ log.warn("Cannot read httpd.robotsFile, using default");
+ file = null;
+ }
+ robotsFile = file;
+ }
+
+ @Override
+ protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp)
+ throws IOException {
+ rsp.setContentType("text/plain");
+ InputStream in = openRobotsFile();
+ try {
+ OutputStream out = rsp.getOutputStream();
+ try {
+ ByteStreams.copy(in, out);
+ } finally {
+ out.close();
+ }
+ } finally {
+ in.close();
+ }
+ }
+
+ private InputStream openRobotsFile() {
+ if (robotsFile != null) {
+ try {
+ return new FileInputStream(robotsFile);
+ } catch (IOException e) {
+ log.warn("Cannot read " + robotsFile + "; using default", e);
+ }
+ }
+ return getServletContext().getResourceAsStream("/robots.txt");
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
index bc0a174..b74d1ac 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
@@ -14,37 +14,60 @@
package com.google.gerrit.httpd.raw;
+import static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
+import static com.google.common.net.HttpHeaders.ETAG;
+import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
import com.google.common.collect.Maps;
+import com.google.common.hash.Hashing;
+import com.google.common.io.ByteStreams;
+import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.gwtjsonrpc.server.RPCServletUtils;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
-import java.util.concurrent.TimeUnit;
-import java.util.zip.GZIPOutputStream;
+import java.util.concurrent.ExecutionException;
+import javax.annotation.Nullable;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+
/** Sends static content from the site 's <code>static/</code> subdirectory. */
@SuppressWarnings("serial")
@Singleton
public class StaticServlet extends HttpServlet {
+ private static final Logger log = LoggerFactory.getLogger(StaticServlet.class);
+ private static final String JS = "application/x-javascript";
private static final Map<String, String> MIME_TYPES = Maps.newHashMap();
static {
MIME_TYPES.put("html", "text/html");
MIME_TYPES.put("htm", "text/html");
- MIME_TYPES.put("js", "application/x-javascript");
+ MIME_TYPES.put("js", JS);
MIME_TYPES.put("css", "text/css");
MIME_TYPES.put("rtf", "text/rtf");
MIME_TYPES.put("txt", "text/plain");
@@ -66,31 +89,13 @@
return type != null ? type : "application/octet-stream";
}
- private static byte[] readFile(final File p) throws IOException {
- final FileInputStream in = new FileInputStream(p);
- try {
- final byte[] r = new byte[(int) in.getChannel().size()];
- IO.readFully(in, r, 0, r.length);
- return r;
- } finally {
- in.close();
- }
- }
-
- private static byte[] compress(final byte[] raw) throws IOException {
- final ByteArrayOutputStream out = new ByteArrayOutputStream();
- final GZIPOutputStream gz = new GZIPOutputStream(out);
- gz.write(raw);
- gz.finish();
- gz.flush();
- return out.toByteArray();
- }
-
private final File staticBase;
private final String staticBasePath;
+ private final boolean refresh;
+ private final LoadingCache<String, Resource> cache;
@Inject
- StaticServlet(final SitePaths site) {
+ StaticServlet(@GerritServerConfig Config cfg, SitePaths site) {
File f;
try {
f = site.static_dir.getCanonicalFile();
@@ -99,70 +104,101 @@
}
staticBase = f;
staticBasePath = staticBase.getPath() + File.separator;
+ refresh = cfg.getBoolean("site", "refreshHeaderFooter", true);
+ cache = CacheBuilder.newBuilder()
+ .maximumWeight(1 << 20)
+ .weigher(new Weigher<String, Resource>() {
+ @Override
+ public int weigh(String name, Resource r) {
+ return 2 * name.length() + r.raw.length;
+ }
+ })
+ .build(new CacheLoader<String, Resource>() {
+ @Override
+ public Resource load(String name) throws Exception {
+ return loadResource(name);
+ }
+ });
}
- private File local(final HttpServletRequest req) {
- final String name = req.getPathInfo();
- if (name.length() < 2 || !name.startsWith("/") || isUnreasonableName(name)) {
- // Too short to be a valid file name, or doesn't start with
- // the path info separator like we expected.
- //
+ @Nullable
+ Resource getResource(String name) {
+ try {
+ return cache.get(name);
+ } catch (ExecutionException e) {
+ log.warn(String.format("Cannot load static resource %s", name), e);
return null;
}
+ }
- final File p = new File(staticBase, name.substring(1));
-
- // Ensure that the requested file is *actually* within the static dir base.
- try {
- if (!p.getCanonicalFile().getPath().startsWith(staticBasePath))
- return null;
- } catch (IOException e) {
- return null;
+ private Resource getResource(HttpServletRequest req) throws ExecutionException {
+ String name = CharMatcher.is('/').trimFrom(req.getPathInfo());
+ if (isUnreasonableName(name)) {
+ return Resource.NOT_FOUND;
}
- return p.isFile() ? p : null;
+ Resource r = cache.get(name);
+ if (r == Resource.NOT_FOUND) {
+ return Resource.NOT_FOUND;
+ }
+
+ if (refresh && r.isStale()) {
+ cache.invalidate(name);
+ r = cache.get(name);
+ }
+ return r;
}
private static boolean isUnreasonableName(String name) {
- if (name.charAt(name.length() -1) == '/') return true; // no suffix
+ if (name.length() < 1) return true;
if (name.indexOf('\\') >= 0) return true; // no windows/dos stlye paths
if (name.startsWith("../")) return true; // no "../etc/passwd"
if (name.contains("/../")) return true; // no "foo/../etc/passwd"
if (name.contains("/./")) return true; // "foo/./foo" is insane to ask
if (name.contains("//")) return true; // windows UNC path can be "//..."
-
return false; // is a reasonable name
}
@Override
- protected long getLastModified(final HttpServletRequest req) {
- final File p = local(req);
- return p != null ? p.lastModified() : -1;
- }
-
- @Override
protected void doGet(final HttpServletRequest req,
final HttpServletResponse rsp) throws IOException {
- final File p = local(req);
- if (p == null) {
+ Resource r;
+ try {
+ r = getResource(req);
+ } catch (ExecutionException e) {
+ log.warn(String.format(
+ "Cannot load static resource %s",
+ req.getPathInfo()), e);
CacheHeaders.setNotCacheable(rsp);
- rsp.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
return;
}
- final String type = contentType(p.getName());
- final byte[] tosend;
- if (!type.equals("application/x-javascript")
- && RPCServletUtils.acceptsGzipEncoding(req)) {
- rsp.setHeader("Content-Encoding", "gzip");
- tosend = compress(readFile(p));
- } else {
- tosend = readFile(p);
+ String e = req.getParameter("e");
+ if (r == Resource.NOT_FOUND || (e != null && !r.etag.equals(e))) {
+ CacheHeaders.setNotCacheable(rsp);
+ rsp.setStatus(SC_NOT_FOUND);
+ return;
+ } else if (r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
+ rsp.setStatus(SC_NOT_MODIFIED);
+ return;
}
- CacheHeaders.setCacheable(req, rsp, 12, TimeUnit.HOURS);
- rsp.setDateHeader("Last-Modified", p.lastModified());
- rsp.setContentType(type);
+ byte[] tosend = r.raw;
+ if (!r.contentType.equals(JS) && RPCServletUtils.acceptsGzipEncoding(req)) {
+ byte[] gz = HtmlDomUtil.compress(tosend);
+ if ((gz.length + 24) < tosend.length) {
+ rsp.setHeader(CONTENT_ENCODING, "gzip");
+ tosend = gz;
+ }
+ }
+ if (e != null && r.etag.equals(e)) {
+ CacheHeaders.setCacheable(req, rsp, 360, DAYS, false);
+ } else {
+ CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
+ }
+ rsp.setHeader(ETAG, r.etag);
+ rsp.setContentType(r.contentType);
rsp.setContentLength(tosend.length);
final OutputStream out = rsp.getOutputStream();
try {
@@ -171,4 +207,54 @@
out.close();
}
}
+
+ private Resource loadResource(String name) throws IOException {
+ File p = new File(staticBase, name);
+ try {
+ p = p.getCanonicalFile();
+ } catch (IOException e) {
+ return Resource.NOT_FOUND;
+ }
+ if (!p.getPath().startsWith(staticBasePath)) {
+ return Resource.NOT_FOUND;
+ }
+
+ long ts = p.lastModified();
+ FileInputStream in;
+ try {
+ in = new FileInputStream(p);
+ } catch (FileNotFoundException e) {
+ return Resource.NOT_FOUND;
+ }
+
+ byte[] raw;
+ try {
+ raw = ByteStreams.toByteArray(in);
+ } finally {
+ in.close();
+ }
+ return new Resource(p, ts, contentType(name), raw);
+ }
+
+ static class Resource {
+ static final Resource NOT_FOUND = new Resource(null, -1, "", new byte[] {});
+
+ final File src;
+ final long lastModified;
+ final String contentType;
+ final String etag;
+ final byte[] raw;
+
+ Resource(File src, long lastModified, String contentType, byte[] raw) {
+ this.src = src;
+ this.lastModified = lastModified;
+ this.contentType = contentType;
+ this.etag = Hashing.md5().hashBytes(raw).toString();
+ this.raw = raw;
+ }
+
+ boolean isStale() {
+ return lastModified != src.lastModified();
+ }
+ }
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index dabffc0..9183d5c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -33,6 +33,7 @@
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
+import com.google.gwtexpui.server.CacheHeaders;
import com.google.inject.Inject;
import org.kohsuke.args4j.CmdLineException;
@@ -67,7 +68,7 @@
clp.parseOptionMap(in);
} catch (CmdLineException e) {
if (!clp.wasHelpRequestedByOption()) {
- replyError(res, SC_BAD_REQUEST, e.getMessage());
+ replyError(req, res, SC_BAD_REQUEST, e.getMessage());
return false;
}
}
@@ -79,6 +80,7 @@
msg.write('\n');
clp.printUsage(msg, null);
msg.write('\n');
+ CacheHeaders.setNotCacheable(res);
replyBinaryResult(req, res,
BinaryResult.create(msg.toString()).setContentType("text/plain"));
return false;
@@ -159,7 +161,6 @@
*/
static JsonObject formToJson(HttpServletRequest req)
throws BadRequestException {
- @SuppressWarnings("unchecked")
Map<String, String[]> map = req.getParameterMap();
return formToJson(map, query(req));
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 1040da3..d8a2648 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkNotNull;
+import static java.math.RoundingMode.CEILING;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
import static javax.servlet.http.HttpServletResponse.SC_CREATED;
@@ -23,9 +24,11 @@
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
+import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
@@ -38,21 +41,23 @@
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
+import com.google.common.io.BaseEncoding;
+import com.google.common.math.IntMath;
import com.google.common.net.HttpHeaders;
import com.google.gerrit.audit.AuditService;
import com.google.gerrit.audit.HttpAuditEvent;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AcceptsCreate;
import com.google.gerrit.extensions.restapi.AcceptsPost;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.CacheControl;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.PreconditionFailedException;
-import com.google.gerrit.extensions.restapi.PutInput;
+import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
@@ -61,7 +66,6 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestResource;
import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.extensions.restapi.StreamingResponse;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.httpd.WebSession;
@@ -70,7 +74,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.OptionUtil;
import com.google.gerrit.server.OutputFormat;
-import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.CapabilityUtils;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.FieldNamingPolicy;
@@ -105,6 +109,7 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
+import java.sql.Timestamp;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -181,7 +186,6 @@
protected final void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
long auditStartTs = System.currentTimeMillis();
- CacheHeaders.setNotCacheable(res);
res.setHeader("Content-Disposition", "attachment");
res.setHeader("X-Content-Type-Options", "nosniff");
int status = SC_OK;
@@ -194,17 +198,18 @@
List<IdString> path = splitPath(req);
RestCollection<RestResource, RestResource> rc = members.get();
- checkAccessAnnotations(rc.getClass());
+ CapabilityUtils.checkRequiresCapability(globals.currentUser,
+ null, rc.getClass());
RestResource rsrc = TopLevelResource.INSTANCE;
- RestView<RestResource> view = null;
+ ViewData viewData = new ViewData(null, null);
if (path.isEmpty()) {
if ("GET".equals(req.getMethod())) {
- view = rc.list();
+ viewData = new ViewData(null, rc.list());
} else if (rc instanceof AcceptsPost && "POST".equals(req.getMethod())) {
@SuppressWarnings("unchecked")
AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) rc;
- view = ac.post(rsrc);
+ viewData = new ViewData(null, ac.post(rsrc));
} else {
throw new MethodNotAllowedException();
}
@@ -212,7 +217,9 @@
IdString id = path.remove(0);
try {
rsrc = rc.parse(rsrc, id);
- checkPreconditions(req, rsrc);
+ if (path.isEmpty()) {
+ checkPreconditions(req, rsrc);
+ }
} catch (ResourceNotFoundException e) {
if (rc instanceof AcceptsCreate
&& path.isEmpty()
@@ -220,30 +227,30 @@
|| "PUT".equals(req.getMethod()))) {
@SuppressWarnings("unchecked")
AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) rc;
- view = ac.create(rsrc, id);
+ viewData = new ViewData(null, ac.create(rsrc, id));
status = SC_CREATED;
} else {
throw e;
}
}
- if (view == null) {
- view = view(rc, req.getMethod(), path);
+ if (viewData.view == null) {
+ viewData = view(rc, req.getMethod(), path);
}
}
- checkAccessAnnotations(view.getClass());
+ checkRequiresCapability(viewData);
- while (view instanceof RestCollection<?,?>) {
+ while (viewData.view instanceof RestCollection<?,?>) {
@SuppressWarnings("unchecked")
RestCollection<RestResource, RestResource> c =
- (RestCollection<RestResource, RestResource>) view;
+ (RestCollection<RestResource, RestResource>) viewData.view;
if (path.isEmpty()) {
if ("GET".equals(req.getMethod())) {
- view = c.list();
+ viewData = new ViewData(null, c.list());
} else if (c instanceof AcceptsPost && "POST".equals(req.getMethod())) {
@SuppressWarnings("unchecked")
AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) c;
- view = ac.post(rsrc);
+ viewData = new ViewData(null, ac.post(rsrc));
} else {
throw new MethodNotAllowedException();
}
@@ -253,7 +260,7 @@
try {
rsrc = c.parse(rsrc, id);
checkPreconditions(req, rsrc);
- view = null;
+ viewData = new ViewData(null, null);
} catch (ResourceNotFoundException e) {
if (c instanceof AcceptsCreate
&& path.isEmpty()
@@ -261,53 +268,58 @@
|| "PUT".equals(req.getMethod()))) {
@SuppressWarnings("unchecked")
AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) c;
- view = ac.create(rsrc, id);
+ viewData = new ViewData(null, ac.create(rsrc, id));
status = SC_CREATED;
} else {
throw e;
}
}
- if (view == null) {
- view = view(c, req.getMethod(), path);
+ if (viewData.view == null) {
+ viewData = view(c, req.getMethod(), path);
}
}
- checkAccessAnnotations(view.getClass());
+ checkRequiresCapability(viewData);
+ }
+
+ if (notModified(req, rsrc)) {
+ res.sendError(SC_NOT_MODIFIED);
+ return;
}
Multimap<String, String> config = LinkedHashMultimap.create();
ParameterParser.splitQueryString(req.getQueryString(), config, params);
- if (!globals.paramParser.get().parse(view, params, req, res)) {
+ if (!globals.paramParser.get().parse(viewData.view, params, req, res)) {
return;
}
- if (view instanceof RestModifyView<?, ?>) {
+ if (viewData.view instanceof RestModifyView<?, ?>) {
@SuppressWarnings("unchecked")
RestModifyView<RestResource, Object> m =
- (RestModifyView<RestResource, Object>) view;
+ (RestModifyView<RestResource, Object>) viewData.view;
inputRequestBody = parseRequest(req, inputType(m));
result = m.apply(rsrc, inputRequestBody);
- } else if (view instanceof RestReadView<?>) {
- result = ((RestReadView<RestResource>) view).apply(rsrc);
+ } else if (viewData.view instanceof RestReadView<?>) {
+ result = ((RestReadView<RestResource>) viewData.view).apply(rsrc);
} else {
throw new ResourceNotFoundException();
}
if (result instanceof Response) {
@SuppressWarnings("rawtypes")
- Response r = (Response) result;
+ Response<?> r = (Response) result;
status = r.statusCode();
+ configureCaching(req, res, rsrc, r.caching());
} else if (result instanceof Response.Redirect) {
+ CacheHeaders.setNotCacheable(res);
res.sendRedirect(((Response.Redirect) result).location());
return;
+ } else {
+ CacheHeaders.setNotCacheable(res);
}
res.setStatus(status);
- if (result instanceof StreamingResponse) {
- StreamingResponse r = (StreamingResponse) result;
- res.setContentType(r.getContentType());
- r.stream(res.getOutputStream());
- } else if (result != Response.none()) {
+ if (result != Response.none()) {
result = Response.unwrap(result);
if (result instanceof BinaryResult) {
replyBinaryResult(req, res, (BinaryResult) result);
@@ -316,27 +328,27 @@
}
}
} catch (AuthException e) {
- replyError(res, status = SC_FORBIDDEN, e.getMessage());
+ replyError(req, res, status = SC_FORBIDDEN, e.getMessage(), e.caching());
} catch (BadRequestException e) {
- replyError(res, status = SC_BAD_REQUEST, e.getMessage());
+ replyError(req, res, status = SC_BAD_REQUEST, e.getMessage(), e.caching());
} catch (MethodNotAllowedException e) {
- replyError(res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed");
+ replyError(req, res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed", e.caching());
} catch (ResourceConflictException e) {
- replyError(res, status = SC_CONFLICT, e.getMessage());
+ replyError(req, res, status = SC_CONFLICT, e.getMessage(), e.caching());
} catch (PreconditionFailedException e) {
- replyError(res, status = SC_PRECONDITION_FAILED,
- Objects.firstNonNull(e.getMessage(), "Precondition failed"));
+ replyError(req, res, status = SC_PRECONDITION_FAILED,
+ Objects.firstNonNull(e.getMessage(), "Precondition failed"), e.caching());
} catch (ResourceNotFoundException e) {
- replyError(res, status = SC_NOT_FOUND, "Not found");
+ replyError(req, res, status = SC_NOT_FOUND, "Not found", e.caching());
} catch (UnprocessableEntityException e) {
- replyError(res, status = 422,
- Objects.firstNonNull(e.getMessage(), "Unprocessable Entity"));
+ replyError(req, res, status = 422,
+ Objects.firstNonNull(e.getMessage(), "Unprocessable Entity"), e.caching());
} catch (AmbiguousViewException e) {
- replyError(res, status = SC_NOT_FOUND, e.getMessage());
+ replyError(req, res, status = SC_NOT_FOUND, e.getMessage());
} catch (MalformedJsonException e) {
- replyError(res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+ replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
} catch (JsonParseException e) {
- replyError(res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+ replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
} catch (Exception e) {
status = SC_INTERNAL_SERVER_ERROR;
handleException(e, req, res);
@@ -348,6 +360,68 @@
}
}
+ private static boolean notModified(HttpServletRequest req, RestResource rsrc) {
+ if (!"GET".equals(req.getMethod())) {
+ return false;
+ }
+
+ if (rsrc instanceof RestResource.HasETag) {
+ String have = req.getHeader(HttpHeaders.IF_NONE_MATCH);
+ if (have != null) {
+ return have.equals(((RestResource.HasETag) rsrc).getETag());
+ }
+ }
+
+ if (rsrc instanceof RestResource.HasLastModified) {
+ Timestamp m = ((RestResource.HasLastModified) rsrc).getLastModified();
+ long d = req.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
+
+ // HTTP times are in seconds, database may have millisecond precision.
+ return d / 1000L == m.getTime() / 1000L;
+ }
+ return false;
+ }
+
+ private static <T> void configureCaching(HttpServletRequest req,
+ HttpServletResponse res, RestResource rsrc, CacheControl c) {
+ if ("GET".equals(req.getMethod())) {
+ switch (c.getType()) {
+ case NONE:
+ default:
+ CacheHeaders.setNotCacheable(res);
+ break;
+ case PRIVATE:
+ addResourceStateHeaders(res, rsrc);
+ CacheHeaders.setCacheablePrivate(res,
+ c.getAge(), c.getUnit(),
+ c.isMustRevalidate());
+ break;
+ case PUBLIC:
+ addResourceStateHeaders(res, rsrc);
+ CacheHeaders.setCacheable(req, res,
+ c.getAge(), c.getUnit(),
+ c.isMustRevalidate());
+ break;
+ }
+ } else {
+ CacheHeaders.setNotCacheable(res);
+ }
+ }
+
+ private static void addResourceStateHeaders(
+ HttpServletResponse res, RestResource rsrc) {
+ if (rsrc instanceof RestResource.HasETag) {
+ res.setHeader(
+ HttpHeaders.ETAG,
+ ((RestResource.HasETag) rsrc).getETag());
+ }
+ if (rsrc instanceof RestResource.HasLastModified) {
+ res.setDateHeader(
+ HttpHeaders.LAST_MODIFIED,
+ ((RestResource.HasLastModified) rsrc).getLastModified().getTime());
+ }
+ }
+
private void checkPreconditions(HttpServletRequest req, RestResource rsrc)
throws PreconditionFailedException {
if ("*".equals(req.getHeader("If-None-Match"))) {
@@ -414,8 +488,9 @@
} finally {
br.close();
}
- } else if ("PUT".equals(req.getMethod()) && acceptsPutInput(type)) {
- return parsePutInput(req, type);
+ } else if (("PUT".equals(req.getMethod()) || "POST".equals(req.getMethod()))
+ && acceptsRawInput(type)) {
+ return parseRawInput(req, type);
} else if ("DELETE".equals(req.getMethod()) && hasNoBody(req)) {
return null;
} else if (hasNoBody(req)) {
@@ -451,10 +526,10 @@
}
@SuppressWarnings("rawtypes")
- private static boolean acceptsPutInput(Type type) {
+ private static boolean acceptsRawInput(Type type) {
if (type instanceof Class) {
for (Field f : ((Class) type).getDeclaredFields()) {
- if (f.getType() == PutInput.class) {
+ if (f.getType() == RawInput.class) {
return true;
}
}
@@ -462,15 +537,15 @@
return false;
}
- private Object parsePutInput(final HttpServletRequest req, Type type)
+ private Object parseRawInput(final HttpServletRequest req, Type type)
throws SecurityException, NoSuchMethodException,
IllegalArgumentException, InstantiationException, IllegalAccessException,
InvocationTargetException, MethodNotAllowedException {
Object obj = createInstance(type);
for (Field f : obj.getClass().getDeclaredFields()) {
- if (f.getType() == PutInput.class) {
+ if (f.getType() == RawInput.class) {
f.setAccessible(true);
- f.set(obj, new PutInput() {
+ f.set(obj, new RawInput() {
@Override
public String getContentType() {
return req.getContentType();
@@ -534,7 +609,7 @@
Multimap<String, String> config,
Object result)
throws IOException {
- final TemporaryBuffer.Heap buf = heap(Integer.MAX_VALUE);
+ TemporaryBuffer.Heap buf = heap(Integer.MAX_VALUE);
buf.write(JSON_MAGIC);
Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
Gson gson = newGson(config, req);
@@ -545,18 +620,9 @@
}
w.write('\n');
w.flush();
-
- replyBinaryResult(req, res, new BinaryResult() {
- @Override
- public long getContentLength() {
- return buf.length();
- }
-
- @Override
- public void writeTo(OutputStream os) throws IOException {
- buf.writeTo(os, null);
- }
- }.setContentType(JSON_TYPE).setCharacterEncoding(UTF_8.name()));
+ replyBinaryResult(req, res, asBinaryResult(buf)
+ .setContentType(JSON_TYPE)
+ .setCharacterEncoding(UTF_8.name()));
}
private static Gson newGson(Multimap<String, String> config,
@@ -626,47 +692,86 @@
@Nullable HttpServletRequest req,
HttpServletResponse res,
BinaryResult bin) throws IOException {
+ final BinaryResult appResult = bin;
try {
+ if (bin.getAttachmentName() != null) {
+ res.setHeader(
+ "Content-Disposition",
+ "attachment; filename=\"" + bin.getAttachmentName() + "\"");
+ }
+ if (bin.isBase64()) {
+ bin = stackBase64(res, bin);
+ }
+ if (bin.canGzip() && acceptsGzip(req)) {
+ bin = stackGzip(res, bin);
+ }
+
res.setContentType(bin.getContentType());
+ long len = bin.getContentLength();
+ if (0 <= len && len < Integer.MAX_VALUE) {
+ res.setContentLength((int) len);
+ } else if (0 <= len) {
+ res.setHeader("Content-Length", Long.toString(len));
+ }
+
OutputStream dst = res.getOutputStream();
try {
- long len = bin.getContentLength();
- boolean gzip = bin.canGzip() && acceptsGzip(req);
- if (gzip && 256 <= len && len <= (10 << 20)) {
- TemporaryBuffer.Heap buf = compress(bin);
- if (buf.length() < len) {
- res.setContentLength((int) buf.length());
- res.setHeader("Content-Encoding", "gzip");
- buf.writeTo(dst, null);
- } else {
- replyUncompressed(res, dst, bin, len);
- }
- } else if (gzip) {
- res.setHeader("Content-Encoding", "gzip");
- dst = new GZIPOutputStream(dst);
- bin.writeTo(dst);
- } else {
- replyUncompressed(res, dst, bin, len);
- }
+ bin.writeTo(dst);
} finally {
dst.close();
}
} finally {
- bin.close();
+ appResult.close();
}
}
- private static void replyUncompressed(HttpServletResponse res,
- OutputStream dst, BinaryResult bin, long len) throws IOException {
- if (0 <= len && len < Integer.MAX_VALUE) {
- res.setContentLength((int) len);
- } else if (0 <= len) {
- res.setHeader("Content-Length", Long.toString(len));
+ private static BinaryResult stackBase64(HttpServletResponse res,
+ final BinaryResult src) throws IOException {
+ BinaryResult b64;
+ long len = src.getContentLength();
+ if (0 <= len && len <= (7 << 20)) {
+ b64 = base64(src);
+ } else {
+ b64 = new BinaryResult() {
+ @Override
+ public void writeTo(OutputStream out) throws IOException {
+ OutputStream e = BaseEncoding.base64().encodingStream(
+ new OutputStreamWriter(out, Charsets.ISO_8859_1));
+ src.writeTo(e);
+ e.flush();
+ }
+ };
}
- bin.writeTo(dst);
+ res.setHeader("X-FYI-Content-Encoding", "base64");
+ res.setHeader("X-FYI-Content-Type", src.getContentType());
+ return b64.setContentType("text/plain").setCharacterEncoding("ISO-8859-1");
}
- private RestView<RestResource> view(
+ private static BinaryResult stackGzip(HttpServletResponse res,
+ final BinaryResult src) throws IOException {
+ BinaryResult gz;
+ long len = src.getContentLength();
+ if (256 <= len && len <= (10 << 20)) {
+ gz = compress(src);
+ if (len <= gz.getContentLength()) {
+ return src;
+ }
+ } else {
+ gz = new BinaryResult() {
+ @Override
+ public void writeTo(OutputStream out) throws IOException {
+ GZIPOutputStream gz = new GZIPOutputStream(out);
+ src.writeTo(gz);
+ gz.finish();
+ gz.flush();
+ }
+ };
+ }
+ res.setHeader("Content-Encoding", "gzip");
+ return gz.setContentType(src.getContentType());
+ }
+
+ private ViewData view(
RestCollection<RestResource, RestResource> rc,
String method, List<IdString> path) throws ResourceNotFoundException,
MethodNotAllowedException, AmbiguousViewException {
@@ -686,7 +791,7 @@
RestView<RestResource> view =
views.get(p.get(0), method + "." + p.get(1));
if (view != null) {
- return view;
+ return new ViewData(p.get(0), view);
}
throw new ResourceNotFoundException(projection);
}
@@ -694,7 +799,7 @@
String name = method + "." + p.get(0);
RestView<RestResource> core = views.get("gerrit", name);
if (core != null) {
- return core;
+ return new ViewData(null, core);
}
Map<String, RestView<RestResource>> r = Maps.newTreeMap();
@@ -706,12 +811,14 @@
}
if (r.size() == 1) {
- return Iterables.getFirst(r.values(), null);
+ Map.Entry<String, RestView<RestResource>> entry =
+ Iterables.getOnlyElement(r.entrySet());
+ return new ViewData(entry.getKey(), entry.getValue());
} else if (r.isEmpty()) {
throw new ResourceNotFoundException(projection);
} else {
throw new AmbiguousViewException(String.format(
- "Projection %s is ambiguous: ",
+ "Projection %s is ambiguous: %s",
name,
Joiner.on(", ").join(
Iterables.transform(r.keySet(), new Function<String, String>() {
@@ -762,18 +869,9 @@
return !("GET".equals(method) || "HEAD".equals(method));
}
- private void checkAccessAnnotations(Class<? extends Object> clazz)
- throws AuthException {
- RequiresCapability rc = clazz.getAnnotation(RequiresCapability.class);
- if (rc != null) {
- CurrentUser user = globals.currentUser.get();
- CapabilityControl ctl = user.getCapabilities();
- if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
- throw new AuthException(String.format(
- "Capability %s is required to access this resource",
- rc.value()));
- }
- }
+ private void checkRequiresCapability(ViewData viewData) throws AuthException {
+ CapabilityUtils.checkRequiresCapability(globals.currentUser,
+ viewData.pluginName, viewData.view.getClass());
}
private static void handleException(Throwable err, HttpServletRequest req,
@@ -786,13 +884,20 @@
if (!res.isCommitted()) {
res.reset();
- replyError(res, SC_INTERNAL_SERVER_ERROR, "Internal server error");
+ replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error");
}
}
- static void replyError(HttpServletResponse res, int statusCode, String msg)
- throws IOException {
+ public static void replyError(HttpServletRequest req,
+ HttpServletResponse res, int statusCode, String msg) throws IOException {
+ replyError(req, res, statusCode, msg, CacheControl.NONE);
+ }
+
+ public static void replyError(HttpServletRequest req,
+ HttpServletResponse res, int statusCode, String msg,
+ CacheControl c) throws IOException {
res.setStatus(statusCode);
+ configureCaching(req, res, null, c);
replyText(null, res, msg);
}
@@ -842,14 +947,33 @@
return false;
}
- private static TemporaryBuffer.Heap compress(BinaryResult bin)
+ private static BinaryResult base64(BinaryResult bin)
+ throws IOException {
+ int max = 4 * IntMath.divide((int) bin.getContentLength(), 3, CEILING);
+ TemporaryBuffer.Heap buf = heap(max);
+ OutputStream encoded = BaseEncoding.base64().encodingStream(
+ new OutputStreamWriter(buf, Charsets.ISO_8859_1));
+ bin.writeTo(encoded);
+ encoded.close();
+ return asBinaryResult(buf);
+ }
+
+ private static BinaryResult compress(BinaryResult bin)
throws IOException {
TemporaryBuffer.Heap buf = heap(20 << 20);
GZIPOutputStream gz = new GZIPOutputStream(buf);
bin.writeTo(gz);
- gz.finish();
- gz.flush();
- return buf;
+ gz.close();
+ return asBinaryResult(buf).setContentType(bin.getContentType());
+ }
+
+ private static BinaryResult asBinaryResult(final TemporaryBuffer.Heap buf) {
+ return new BinaryResult() {
+ @Override
+ public void writeTo(OutputStream os) throws IOException {
+ buf.writeTo(os, null);
+ }
+ }.setContentLength(buf.length());
}
private static Heap heap(int max) {
@@ -862,4 +986,14 @@
super(message);
}
}
+
+ private static class ViewData {
+ String pluginName;
+ RestView<RestResource> view;
+
+ ViewData(String pluginName, RestView<RestResource> view) {
+ this.pluginName = pluginName;
+ this.view = view;
+ }
+ }
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java
index 059b54c..c0c4535 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java
@@ -28,7 +28,7 @@
super(response);
}
- int getStatus() {
+ public int getStatus() {
return status;
}
@@ -39,6 +39,7 @@
}
@Override
+ @Deprecated
public void setStatus(int sc, String sm) {
super.setStatus(sc, sm);
this.status = sc;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
index 3277992..940e2e6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
@@ -42,7 +42,7 @@
protected Account.Id getAccountId() {
CurrentUser u = currentUser.get();
- if (u instanceof IdentifiedUser) {
+ if (u.isIdentifiedUser()) {
return ((IdentifiedUser) u).getAccountId();
}
return null;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
index 22546a7..7690f5c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
@@ -46,6 +46,7 @@
import org.eclipse.jgit.lib.Config;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -218,7 +219,8 @@
final String query, final int limit,
final AsyncCallback<List<ReviewerInfo>> callback) {
run(callback, new Action<List<ReviewerInfo>>() {
- public List<ReviewerInfo> run(final ReviewDb db) throws OrmException {
+ public List<ReviewerInfo> run(final ReviewDb db)
+ throws OrmException, Failure {
final ChangeControl changeControl;
try {
changeControl = changeControlFactory.controlFor(change);
@@ -273,7 +275,7 @@
}
private boolean suggestGroupAsReviewer(final Project.NameKey project,
- final GroupReference group) throws OrmException {
+ final GroupReference group) throws OrmException, Failure {
if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
return false;
}
@@ -296,6 +298,8 @@
return false;
} catch (NoSuchProjectException e) {
return false;
+ } catch (IOException e) {
+ throw new Failure(e);
}
return true;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/access/AccessRestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/access/AccessRestApiServlet.java
new file mode 100644
index 0000000..fda6416
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/access/AccessRestApiServlet.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.rpc.access;
+
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.access.AccessCollection;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class AccessRestApiServlet extends RestApiServlet {
+ private static final long serialVersionUID = 1L;
+
+ @Inject
+ AccessRestApiServlet(RestApiServlet.Globals globals,
+ Provider<AccessCollection> access) {
+ super(globals, access);
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
index bef6316..d0fb504 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
@@ -17,7 +17,6 @@
import com.google.gerrit.httpd.rpc.RpcServletModule;
import com.google.gerrit.httpd.rpc.UiRpcModule;
import com.google.gerrit.server.config.FactoryModule;
-import com.google.gerrit.server.mail.RegisterNewEmailSender;
public class AccountModule extends RpcServletModule {
public AccountModule() {
@@ -32,7 +31,6 @@
factory(AgreementInfoFactory.Factory.class);
factory(DeleteExternalIds.Factory.class);
factory(ExternalIdDetailFactory.Factory.class);
- factory(RegisterNewEmailSender.Factory.class);
}
});
rpc(AccountSecurityImpl.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index 6d183b8..d3e0f84 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -19,8 +19,6 @@
import com.google.gerrit.common.data.AccountSecurity;
import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.errors.ContactInformationStoreException;
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.common.errors.PermissionDeniedException;
import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
@@ -30,8 +28,6 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.client.ContactInformation;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
@@ -40,49 +36,34 @@
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.ChangeUserName;
-import com.google.gerrit.server.account.ClearPassword;
-import com.google.gerrit.server.account.GeneratePassword;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.contact.ContactStore;
import com.google.gerrit.server.mail.EmailTokenVerifier;
-import com.google.gerrit.server.mail.RegisterNewEmailSender;
import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import java.util.Collections;
import java.util.List;
import java.util.Set;
class AccountSecurityImpl extends BaseServiceImplementation implements
AccountSecurity {
- private final Logger log = LoggerFactory.getLogger(getClass());
private final ContactStore contactStore;
- private final AuthConfig authConfig;
private final Realm realm;
private final ProjectCache projectCache;
private final Provider<IdentifiedUser> user;
private final EmailTokenVerifier emailTokenVerifier;
- private final RegisterNewEmailSender.Factory registerNewEmailFactory;
- private final SshKeyCache sshKeyCache;
private final AccountByEmailCache byEmailCache;
private final AccountCache accountCache;
private final AccountManager accountManager;
private final boolean useContactInfo;
- private final ClearPassword.Factory clearPasswordFactory;
- private final GeneratePassword.Factory generatePasswordFactory;
private final ChangeUserName.CurrentUser changeUserNameFactory;
private final DeleteExternalIds.Factory deleteExternalIdsFactory;
private final ExternalIdDetailFactory.Factory externalIdDetailFactory;
@@ -93,34 +74,26 @@
@Inject
AccountSecurityImpl(final Provider<ReviewDb> schema,
final Provider<CurrentUser> currentUser, final ContactStore cs,
- final AuthConfig ac, final Realm r, final Provider<IdentifiedUser> u,
+ final Realm r, final Provider<IdentifiedUser> u,
final EmailTokenVerifier etv, final ProjectCache pc,
- final RegisterNewEmailSender.Factory esf, final SshKeyCache skc,
final AccountByEmailCache abec, final AccountCache uac,
final AccountManager am,
- final ClearPassword.Factory clearPasswordFactory,
- final GeneratePassword.Factory generatePasswordFactory,
final ChangeUserName.CurrentUser changeUserNameFactory,
final DeleteExternalIds.Factory deleteExternalIdsFactory,
final ExternalIdDetailFactory.Factory externalIdDetailFactory,
final ChangeHooks hooks, final GroupCache groupCache) {
super(schema, currentUser);
contactStore = cs;
- authConfig = ac;
realm = r;
user = u;
emailTokenVerifier = etv;
projectCache = pc;
- registerNewEmailFactory = esf;
- sshKeyCache = skc;
byEmailCache = abec;
accountCache = uac;
accountManager = am;
useContactInfo = contactStore != null && contactStore.isEnabled();
- this.clearPasswordFactory = clearPasswordFactory;
- this.generatePasswordFactory = generatePasswordFactory;
this.changeUserNameFactory = changeUserNameFactory;
this.deleteExternalIdsFactory = deleteExternalIdsFactory;
this.externalIdDetailFactory = externalIdDetailFactory;
@@ -128,60 +101,6 @@
this.groupCache = groupCache;
}
- public void mySshKeys(final AsyncCallback<List<AccountSshKey>> callback) {
- run(callback, new Action<List<AccountSshKey>>() {
- public List<AccountSshKey> run(ReviewDb db) throws OrmException {
- IdentifiedUser u = user.get();
- return db.accountSshKeys().byAccount(u.getAccountId()).toList();
- }
- });
- }
-
- public void addSshKey(final String keyText,
- final AsyncCallback<AccountSshKey> callback) {
- run(callback, new Action<AccountSshKey>() {
- public AccountSshKey run(final ReviewDb db) throws OrmException, Failure {
- int max = 0;
- final Account.Id me = user.get().getAccountId();
- for (final AccountSshKey k : db.accountSshKeys().byAccount(me)) {
- max = Math.max(max, k.getKey().get());
- }
-
- final AccountSshKey key;
- try {
- key = sshKeyCache.create(new AccountSshKey.Id(me, max + 1), keyText);
- } catch (InvalidSshKeyException e) {
- throw new Failure(e);
- }
- db.accountSshKeys().insert(Collections.singleton(key));
- uncacheSshKeys();
- return key;
- }
- });
- }
-
- public void deleteSshKeys(final Set<AccountSshKey.Id> ids,
- final AsyncCallback<VoidResult> callback) {
- run(callback, new Action<VoidResult>() {
- public VoidResult run(final ReviewDb db) throws OrmException, Failure {
- final Account.Id me = user.get().getAccountId();
- for (final AccountSshKey.Id keyId : ids) {
- if (!me.equals(keyId.getParentKey()))
- throw new Failure(new NoSuchEntityException());
- }
-
- db.accountSshKeys().deleteKeys(ids);
- uncacheSshKeys();
-
- return VoidResult.INSTANCE;
- }
- });
- }
-
- private void uncacheSshKeys() {
- sshKeyCache.evict(user.get().getUserName());
- }
-
@Override
public void changeUserName(final String newName,
final AsyncCallback<VoidResult> callback) {
@@ -193,18 +112,6 @@
}
}
- @Override
- public void generatePassword(AccountExternalId.Key key,
- AsyncCallback<AccountExternalId> callback) {
- Handler.wrap(generatePasswordFactory.create(key)).to(callback);
- }
-
- @Override
- public void clearPassword(AccountExternalId.Key key,
- AsyncCallback<AccountExternalId> callback) {
- Handler.wrap(clearPasswordFactory.create(key)).to(callback);
- }
-
public void myExternalIds(AsyncCallback<List<AccountExternalId>> callback) {
externalIdDetailFactory.create().to(callback);
}
@@ -302,31 +209,6 @@
});
}
- public void registerEmail(final String address,
- final AsyncCallback<Account> cb) {
- if (authConfig.getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
- try {
- accountManager.link(user.get().getAccountId(),
- AuthRequest.forEmail(address));
- cb.onSuccess(user.get().getAccount());
- } catch (AccountException e) {
- cb.onFailure(e);
- }
- } else {
- try {
- final RegisterNewEmailSender sender;
- sender = registerNewEmailFactory.create(address);
- sender.send();
- } catch (EmailException e) {
- log.error("Cannot send email verification message to " + address, e);
- cb.onFailure(e);
- } catch (RuntimeException e) {
- log.error("Cannot send email verification message to " + address, e);
- cb.onFailure(e);
- }
- }
- }
-
public void validateEmail(final String tokenString,
final AsyncCallback<VoidResult> callback) {
try {
@@ -342,6 +224,8 @@
callback.onFailure(e);
} catch (AccountException e) {
callback.onFailure(e);
+ } catch (OrmException e) {
+ callback.onFailure(e);
}
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index 2fe3124..0a9ed28 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
@@ -154,7 +154,7 @@
if (filter != null) {
try {
ChangeQueryBuilder builder = queryBuilder.create(currentUser.get());
- builder.setAllowFile(true);
+ builder.setAllowFileRegex(true);
builder.parse(filter);
} catch (QueryParseException badFilter) {
throw new InvalidQueryException(badFilter.getMessage(), filter);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index 557e017..36c9ff1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
@@ -18,6 +18,9 @@
import com.google.gerrit.common.data.ChangeInfo;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
@@ -27,15 +30,16 @@
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.AnonymousUser;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.Mergeable;
+import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.changedetail.RebaseChange;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -48,6 +52,8 @@
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
@@ -61,6 +67,9 @@
/** Creates a {@link ChangeDetail} from a {@link Change}. */
public class ChangeDetailFactory extends Handler<ChangeDetail> {
+ private static final Logger log = LoggerFactory
+ .getLogger(ChangeDetailFactory.class);
+
public interface Factory {
ChangeDetailFactory create(Change.Id id);
}
@@ -78,7 +87,7 @@
private ChangeControl control;
private Map<PatchSet.Id, PatchSet> patchsetsById;
- private final MergeOp.Factory opFactory;
+ private final Mergeable mergeable;
private boolean testMerge;
private List<PatchSetAncestor> currentPatchSetAncestors;
@@ -92,7 +101,7 @@
final ChangeControl.Factory changeControlFactory,
final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
final AnonymousUser anonymousUser,
- final MergeOp.Factory opFactory,
+ final Mergeable mergeable,
@GerritServerConfig final Config cfg,
@Assisted final Change.Id id) {
this.patchSetDetail = patchSetDetail;
@@ -102,7 +111,7 @@
this.anonymousUser = anonymousUser;
this.aic = accountInfoCacheFactory.create();
- this.opFactory = opFactory;
+ this.mergeable = mergeable;
this.testMerge = cfg.getBoolean("changeMerge", "test", false);
this.changeId = id;
@@ -135,7 +144,7 @@
changeId));
detail.setCanRevert(change.getStatus() == Change.Status.MERGED && control.canAddPatchSet());
-
+ detail.setCanCherryPick(control.getProjectControl().canUpload());
detail.setCanEdit(control.getRefControl().canWrite());
detail.setCanEditCommitMessage(change.getStatus().isOpen() && control.canAddPatchSet());
detail.setCanEditTopicName(control.canEditTopicName());
@@ -180,7 +189,7 @@
Set<PatchSet.Id> patchesWithDraftComments = new HashSet<PatchSet.Id>();
final CurrentUser user = control.getCurrentUser();
final Account.Id me =
- user instanceof IdentifiedUser ? ((IdentifiedUser) user).getAccountId()
+ user.isIdentifiedUser() ? ((IdentifiedUser) user).getAccountId()
: null;
for (PatchSet ps : source) {
final PatchSet.Id psId = ps.getId();
@@ -224,7 +233,21 @@
final Change.Status status = detail.getChange().getStatus();
if ((status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT)) &&
testMerge) {
- ChangeUtil.testMerge(opFactory, detail.getChange());
+ try {
+ detail.getChange().setMergeable(mergeable.apply(new RevisionResource(
+ new ChangeResource(control),
+ detail.getCurrentPatchSet())).mergeable);
+ } catch (RepositoryNotFoundException e) {
+ log.warn("Cannot check mergeable", e);
+ } catch (ResourceConflictException e) {
+ log.warn("Cannot check mergeable", e);
+ } catch (BadRequestException e) {
+ log.warn("Cannot check mergeable", e);
+ } catch (AuthException e) {
+ log.warn("Cannot check mergeable", e);
+ } catch (IOException e) {
+ log.warn("Cannot check mergeable", e);
+ }
}
}
@@ -283,7 +306,7 @@
final CurrentUser currentUser = control.getCurrentUser();
Account.Id currentUserId = null;
- if (currentUser instanceof IdentifiedUser) {
+ if (currentUser.isIdentifiedUser()) {
currentUserId = ((IdentifiedUser) currentUser).getAccountId();
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
index ba50417..2706279 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
@@ -14,7 +14,6 @@
package com.google.gerrit.httpd.rpc.changedetail;
-import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.common.errors.NoSuchEntityException;
@@ -25,18 +24,14 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.TrackingFooters;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.mail.CommitMessageEditedSender;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.ssh.NoSshInfo;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -61,21 +56,12 @@
private final IdentifiedUser currentUser;
private final ChangeDetailFactory.Factory changeDetailFactory;
private final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory;
-
- private final GitReferenceUpdated gitRefUpdated;
-
private final PatchSet.Id patchSetId;
@Nullable
private final String message;
-
- private final ChangeHooks hooks;
- private final CommitValidators.Factory commitValidatorsFactory;
-
private final GitRepositoryManager gitManager;
- private final PatchSetInfoFactory patchSetInfoFactory;
-
private final PersonIdent myIdent;
- private final TrackingFooters trackingFooters;
+ private final PatchSetInserter.Factory patchSetInserterFactory;
@Inject
EditCommitMessageHandler(final ChangeControl.Factory changeControlFactory,
@@ -83,29 +69,20 @@
final ChangeDetailFactory.Factory changeDetailFactory,
final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory,
@Assisted final PatchSet.Id patchSetId,
- @Assisted @Nullable final String message, final ChangeHooks hooks,
- final CommitValidators.Factory commitValidatorsFactory,
+ @Assisted @Nullable final String message,
final GitRepositoryManager gitManager,
- final PatchSetInfoFactory patchSetInfoFactory,
- final GitReferenceUpdated gitRefUpdated,
@GerritPersonIdent final PersonIdent myIdent,
- TrackingFooters trackingFooters) {
+ final PatchSetInserter.Factory patchSetInserterFactory) {
this.changeControlFactory = changeControlFactory;
this.db = db;
this.currentUser = currentUser;
this.changeDetailFactory = changeDetailFactory;
this.commitMessageEditedSenderFactory = commitMessageEditedSenderFactory;
-
this.patchSetId = patchSetId;
this.message = message;
- this.hooks = hooks;
- this.commitValidatorsFactory = commitValidatorsFactory;
this.gitManager = gitManager;
-
- this.patchSetInfoFactory = patchSetInfoFactory;
- this.gitRefUpdated = gitRefUpdated;
this.myIdent = myIdent;
- this.trackingFooters = trackingFooters;
+ this.patchSetInserterFactory = patchSetInserterFactory;
}
@Override
@@ -128,13 +105,9 @@
throw new NoSuchChangeException(changeId, e);
}
try {
- CommitValidators commitValidators =
- commitValidatorsFactory.create(control.getRefControl(), new NoSshInfo(), git);
-
- ChangeUtil.editCommitMessage(patchSetId, control.getRefControl(), commitValidators, currentUser, message, db,
- commitMessageEditedSenderFactory, hooks, git, patchSetInfoFactory, gitRefUpdated, myIdent,
- trackingFooters);
-
+ ChangeUtil.editCommitMessage(patchSetId, control.getRefControl(),
+ currentUser, message, db, commitMessageEditedSenderFactory, git,
+ myIdent, patchSetInserterFactory);
return changeDetailFactory.create(changeId).call();
} finally {
git.close();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
index c07ee51..0c7df3e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
@@ -20,6 +20,7 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.IncludedInResolver;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -29,23 +30,15 @@
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
/** Creates a {@link IncludedInDetail} of a {@link Change}. */
class IncludedInDetailFactory extends Handler<IncludedInDetail> {
- private static final Logger log =
- LoggerFactory.getLogger(IncludedInDetailFactory.class);
interface Factory {
IncludedInDetailFactory create(Change.Id id);
@@ -56,7 +49,6 @@
private final GitRepositoryManager repoManager;
private final Change.Id changeId;
- private IncludedInDetail detail;
private ChangeControl control;
@Inject
@@ -92,11 +84,7 @@
throw new InvalidRevisionException();
}
- detail = new IncludedInDetail();
- detail.setBranches(includedIn(repo, rw, rev, Constants.R_HEADS));
- detail.setTags(includedIn(repo, rw, rev, Constants.R_TAGS));
-
- return detail;
+ return IncludedInResolver.resolve(repo, rw, rev);
} finally {
rw.release();
}
@@ -104,32 +92,4 @@
repo.close();
}
}
-
- private List<String> includedIn(final Repository repo, final RevWalk rw,
- final RevCommit rev, final String namespace) throws IOException,
- MissingObjectException, IncorrectObjectTypeException {
- final List<String> result = new ArrayList<String>();
- for (final Ref ref : repo.getRefDatabase().getRefs(namespace).values()) {
- final RevCommit tip;
- try {
- tip = rw.parseCommit(ref.getObjectId());
- } catch (IncorrectObjectTypeException notCommit) {
- // Its OK for a tag reference to point to a blob or a tree, this
- // is common in the Linux kernel or git.git repository.
- //
- continue;
- } catch (MissingObjectException notHere) {
- // Log the problem with this branch, but keep processing.
- //
- log.warn("Reference " + ref.getName() + " in " + repo.getDirectory()
- + " points to dangling object " + ref.getObjectId());
- continue;
- }
-
- if (rw.isMergedInto(rev, tip)) {
- result.add(ref.getName().substring(namespace.length()));
- }
- }
- return result;
- }
-}
+}
\ No newline at end of file
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
index 8e81dd3..74c243e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
@@ -14,20 +14,29 @@
package com.google.gerrit.httpd.rpc.changedetail;
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.common.data.UiCommandDetail;
import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
import com.google.gerrit.reviewdb.client.AccountPatchReview;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.extensions.webui.UiActions;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListKey;
@@ -39,6 +48,7 @@
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import com.google.inject.util.Providers;
import org.eclipse.jgit.lib.ObjectId;
import org.slf4j.Logger;
@@ -67,6 +77,7 @@
private final ReviewDb db;
private final PatchListCache patchListCache;
private final ChangeControl.Factory changeControlFactory;
+ private final Revisions revisions;
private Project.NameKey projectKey;
private final PatchSet.Id psIdBase;
@@ -83,6 +94,7 @@
PatchSetDetailFactory(final PatchSetInfoFactory psif, final ReviewDb db,
final PatchListCache patchListCache,
final ChangeControl.Factory changeControlFactory,
+ final Revisions revisions,
@Assisted("psIdBase") @Nullable final PatchSet.Id psIdBase,
@Assisted("psIdNew") final PatchSet.Id psIdNew,
@Assisted @Nullable final AccountDiffPreference diffPrefs) {
@@ -90,6 +102,7 @@
this.db = db;
this.patchListCache = patchListCache;
this.changeControlFactory = changeControlFactory;
+ this.revisions = revisions;
this.psIdBase = psIdBase;
this.psIdNew = psIdNew;
@@ -143,7 +156,7 @@
detail.setPatches(patches);
final CurrentUser user = control.getCurrentUser();
- if (user instanceof IdentifiedUser) {
+ if (user.isIdentifiedUser()) {
// If we are signed in, compute the number of draft comments by the
// current user on each of these patch files. This way they can more
// quickly locate where they have pending drafts, and review them.
@@ -164,6 +177,23 @@
}
}
+ detail.setCommands(Lists.newArrayList(Iterables.transform(
+ UiActions.sorted(UiActions.plugins(UiActions.from(
+ revisions,
+ new RevisionResource(new ChangeResource(control), patchSet),
+ Providers.of(user)))),
+ new Function<UiAction.Description, UiCommandDetail>() {
+ @Override
+ public UiCommandDetail apply(UiAction.Description in) {
+ UiCommandDetail r = new UiCommandDetail();
+ r.method = in.getMethod();
+ r.id = in.getId();
+ r.label = in.getLabel();
+ r.title = in.getTitle();
+ r.enabled = in.isEnabled();
+ return r;
+ }
+ })));
return detail;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChangeHandler.java
index b9acfa9..8558fe8 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChangeHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChangeHandler.java
@@ -62,7 +62,7 @@
EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException,
MissingObjectException, IncorrectObjectTypeException, IOException,
InvalidChangeOperationException, NoSuchProjectException {
- rebaseChange.rebase(patchSetId, currentUser.getAccountId());
+ rebaseChange.rebase(patchSetId, currentUser);
return changeDetailFactory.create(patchSetId.getParentKey()).call();
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/config/ConfigRestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/config/ConfigRestApiServlet.java
new file mode 100644
index 0000000..f951ad3
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/config/ConfigRestApiServlet.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.rpc.config;
+
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.config.ConfigCollection;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class ConfigRestApiServlet extends RestApiServlet {
+ private static final long serialVersionUID = 1L;
+
+ @Inject
+ ConfigRestApiServlet(RestApiServlet.Globals globals,
+ Provider<ConfigCollection> configCollection) {
+ super(globals, configCollection);
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
index 3174757..35a2b3d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
@@ -20,6 +20,7 @@
import com.google.gerrit.common.data.ReviewResult;
import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
+import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.httpd.rpc.changedetail.ChangeDetailFactory;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
import com.google.gerrit.reviewdb.client.Change;
@@ -29,7 +30,9 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
+import com.google.gerrit.server.patch.PatchScriptFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -49,6 +52,7 @@
private final PatchScriptFactory.Factory patchScriptFactoryFactory;
private final SaveDraft.Factory saveDraftFactory;
private final ChangeDetailFactory.Factory changeDetailFactory;
+ private final ChangeControl.Factory changeControlFactory;
@Inject
PatchDetailServiceImpl(final Provider<ReviewDb> schema,
@@ -56,13 +60,15 @@
final DeleteDraftPatchSet.Factory deleteDraftPatchSetFactory,
final PatchScriptFactory.Factory patchScriptFactoryFactory,
final SaveDraft.Factory saveDraftFactory,
- final ChangeDetailFactory.Factory changeDetailFactory) {
+ final ChangeDetailFactory.Factory changeDetailFactory,
+ final ChangeControl.Factory changeControlFactory) {
super(schema, currentUser);
this.deleteDraftPatchSetFactory = deleteDraftPatchSetFactory;
this.patchScriptFactoryFactory = patchScriptFactoryFactory;
this.saveDraftFactory = saveDraftFactory;
this.changeDetailFactory = changeDetailFactory;
+ this.changeControlFactory = changeControlFactory;
}
public void patchScript(final Patch.Key patchKey, final PatchSet.Id psa,
@@ -72,7 +78,16 @@
callback.onFailure(new NoSuchEntityException());
return;
}
- patchScriptFactoryFactory.create(patchKey, psa, psb, dp).to(callback);
+
+ new Handler<PatchScript>() {
+ @Override
+ public PatchScript call() throws Exception {
+ Change.Id changeId = patchKey.getParentKey().getParentKey();
+ ChangeControl control = changeControlFactory.validateFor(changeId);
+ return patchScriptFactoryFactory.create(
+ control, patchKey.getFileName(), psa, psb, dp).call();
+ }
+ }.to(callback);
}
public void saveDraft(final PatchLineComment comment,
@@ -124,7 +139,7 @@
}
return changeDetailFactory.create(result.getChangeId()).call();
} catch (NoSuchChangeException e) {
- throw new Failure(new NoSuchChangeException(result.getChangeId()));
+ throw new Failure(new NoSuchChangeException(psid.getParentKey()));
} catch (NoSuchProjectException e) {
throw new Failure(e);
} catch (NoSuchEntityException e) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java
index d1f5b24..4e69b14 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java
@@ -28,11 +28,9 @@
install(new FactoryModule() {
@Override
protected void configure() {
- factory(PatchScriptFactory.Factory.class);
factory(SaveDraft.Factory.class);
}
});
- bind(PatchScriptBuilder.class);
rpc(PatchDetailServiceImpl.class);
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
deleted file mode 100644
index 9a4b89f..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
+++ /dev/null
@@ -1,523 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.patch;
-
-import com.google.gerrit.common.data.CommentDetail;
-import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.common.data.PatchScript.DisplayMethod;
-import com.google.gerrit.prettify.common.EditList;
-import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.server.FileTypeRegistry;
-import com.google.gerrit.server.patch.IntraLineDiff;
-import com.google.gerrit.server.patch.IntraLineDiffKey;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListEntry;
-import com.google.gerrit.server.patch.Text;
-import com.google.inject.Inject;
-
-import eu.medsea.mimeutil.MimeType;
-import eu.medsea.mimeutil.MimeUtil2;
-
-import org.eclipse.jgit.diff.Edit;
-import org.eclipse.jgit.errors.CorruptObjectException;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-class PatchScriptBuilder {
- static final int MAX_CONTEXT = 5000000;
- static final int BIG_FILE = 9000;
-
- private static final Comparator<Edit> EDIT_SORT = new Comparator<Edit>() {
- @Override
- public int compare(final Edit o1, final Edit o2) {
- return o1.getBeginA() - o2.getBeginA();
- }
- };
-
- private Repository db;
- private Project.NameKey projectKey;
- private ObjectReader reader;
- private Change change;
- private AccountDiffPreference diffPrefs;
- private boolean againstParent;
- private ObjectId aId;
- private ObjectId bId;
-
- private final Side a;
- private final Side b;
-
- private List<Edit> edits;
- private final FileTypeRegistry registry;
- private final PatchListCache patchListCache;
- private int context;
-
- @Inject
- PatchScriptBuilder(final FileTypeRegistry ftr, final PatchListCache plc) {
- a = new Side();
- b = new Side();
- registry = ftr;
- patchListCache = plc;
- }
-
- void setRepository(Repository r, Project.NameKey projectKey) {
- this.db = r;
- this.projectKey = projectKey;
- }
-
- void setChange(final Change c) {
- this.change = c;
- }
-
- void setDiffPrefs(final AccountDiffPreference dp) {
- diffPrefs = dp;
-
- context = diffPrefs.getContext();
- if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
- context = MAX_CONTEXT;
- } else if (context > MAX_CONTEXT) {
- context = MAX_CONTEXT;
- }
- }
-
- void setTrees(final boolean ap, final ObjectId a, final ObjectId b) {
- againstParent = ap;
- aId = a;
- bId = b;
- }
-
- PatchScript toPatchScript(final PatchListEntry content,
- final CommentDetail comments, final List<Patch> history)
- throws IOException {
- reader = db.newObjectReader();
- try {
- return build(content, comments, history);
- } finally {
- reader.release();
- }
- }
-
- private PatchScript build(final PatchListEntry content,
- final CommentDetail comments, final List<Patch> history)
- throws IOException {
- boolean intralineDifferenceIsPossible = true;
- boolean intralineFailure = false;
- boolean intralineTimeout = false;
-
- a.path = oldName(content);
- b.path = newName(content);
-
- a.resolve(null, aId);
- b.resolve(a, bId);
-
- edits = new ArrayList<Edit>(content.getEdits());
-
- if (!isModify(content)) {
- intralineDifferenceIsPossible = false;
- } else if (diffPrefs.isIntralineDifference()) {
- IntraLineDiff d =
- patchListCache.getIntraLineDiff(new IntraLineDiffKey(a.id, a.src,
- b.id, b.src, edits, projectKey, bId, b.path,
- diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE));
- if (d != null) {
- switch (d.getStatus()) {
- case EDIT_LIST:
- edits = new ArrayList<Edit>(d.getEdits());
- break;
-
- case DISABLED:
- intralineDifferenceIsPossible = false;
- break;
-
- case ERROR:
- intralineDifferenceIsPossible = false;
- intralineFailure = true;
- break;
-
- case TIMEOUT:
- intralineDifferenceIsPossible = false;
- intralineTimeout = true;
- break;
- }
- } else {
- intralineDifferenceIsPossible = false;
- intralineFailure = true;
- }
- }
-
- ensureCommentsVisible(comments);
-
- boolean hugeFile = false;
- if (a.mode == FileMode.GITLINK || b.mode == FileMode.GITLINK) {
-
- } else if (a.src == b.src && a.size() <= context
- && content.getEdits().isEmpty()) {
- // Odd special case; the files are identical (100% rename or copy)
- // and the user has asked for context that is larger than the file.
- // Send them the entire file, with an empty edit after the last line.
- //
- for (int i = 0; i < a.size(); i++) {
- a.addLine(i);
- }
- edits = new ArrayList<Edit>(1);
- edits.add(new Edit(a.size(), a.size()));
-
- } else {
- if (BIG_FILE < Math.max(a.size(), b.size())) {
- // IF the file is really large, we disable things to avoid choking
- // the browser client.
- //
- diffPrefs.setContext((short) Math.min(25, context));
- diffPrefs.setSyntaxHighlighting(false);
- context = diffPrefs.getContext();
- hugeFile = true;
-
- } else {
- // In order to expand the skipped common lines or syntax highlight the
- // file properly we need to give the client the complete file contents.
- // So force our context temporarily to the complete file size.
- //
- context = MAX_CONTEXT;
- }
- packContent(diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE);
- }
-
- return new PatchScript(change.getKey(), content.getChangeType(),
- content.getOldName(), content.getNewName(), a.fileMode, b.fileMode,
- content.getHeaderLines(), diffPrefs, a.dst, b.dst, edits,
- a.displayMethod, b.displayMethod, comments, history, hugeFile,
- intralineDifferenceIsPossible, intralineFailure, intralineTimeout);
- }
-
- private static boolean isModify(PatchListEntry content) {
- switch (content.getChangeType()) {
- case MODIFIED:
- case COPIED:
- case RENAMED:
- return true;
-
- case ADDED:
- case DELETED:
- default:
- return false;
- }
- }
-
- private static String oldName(final PatchListEntry entry) {
- switch (entry.getChangeType()) {
- case ADDED:
- return null;
- case DELETED:
- case MODIFIED:
- return entry.getNewName();
- case COPIED:
- case RENAMED:
- default:
- return entry.getOldName();
- }
- }
-
- private static String newName(final PatchListEntry entry) {
- switch (entry.getChangeType()) {
- case DELETED:
- return null;
- case ADDED:
- case MODIFIED:
- case COPIED:
- case RENAMED:
- default:
- return entry.getNewName();
- }
- }
-
- private void ensureCommentsVisible(final CommentDetail comments) {
- if (comments.getCommentsA().isEmpty() && comments.getCommentsB().isEmpty()) {
- // No comments, no additional dummy edits are required.
- //
- return;
- }
-
- // Construct empty Edit blocks around each location where a comment is.
- // This will force the later packContent method to include the regions
- // containing comments, potentially combining those regions together if
- // they have overlapping contexts. UI renders will also be able to make
- // correct hunks from this, but because the Edit is empty they will not
- // style it specially.
- //
- final List<Edit> empty = new ArrayList<Edit>();
- int lastLine;
-
- lastLine = -1;
- for (PatchLineComment plc : comments.getCommentsA()) {
- final int a = plc.getLine();
- if (lastLine != a) {
- final int b = mapA2B(a - 1);
- if (0 <= b) {
- safeAdd(empty, new Edit(a - 1, b));
- }
- lastLine = a;
- }
- }
-
- lastLine = -1;
- for (PatchLineComment plc : comments.getCommentsB()) {
- final int b = plc.getLine();
- if (lastLine != b) {
- final int a = mapB2A(b - 1);
- if (0 <= a) {
- safeAdd(empty, new Edit(a, b - 1));
- }
- lastLine = b;
- }
- }
-
- // Sort the final list by the index in A, so packContent can combine
- // them correctly later.
- //
- edits.addAll(empty);
- Collections.sort(edits, EDIT_SORT);
- }
-
- private void safeAdd(final List<Edit> empty, final Edit toAdd) {
- final int a = toAdd.getBeginA();
- final int b = toAdd.getBeginB();
- for (final Edit e : edits) {
- if (e.getBeginA() <= a && a <= e.getEndA()) {
- return;
- }
- if (e.getBeginB() <= b && b <= e.getEndB()) {
- return;
- }
- }
- empty.add(toAdd);
- }
-
- private int mapA2B(final int a) {
- if (edits.isEmpty()) {
- // Magic special case of an unmodified file.
- //
- return a;
- }
-
- for (int i = 0; i < edits.size(); i++) {
- final Edit e = edits.get(i);
- if (a < e.getBeginA()) {
- if (i == 0) {
- // Special case of context at start of file.
- //
- return a;
- }
- return e.getBeginB() - (e.getBeginA() - a);
- }
- if (e.getBeginA() <= a && a <= e.getEndA()) {
- return -1;
- }
- }
-
- final Edit last = edits.get(edits.size() - 1);
- return last.getEndB() + (a - last.getEndA());
- }
-
- private int mapB2A(final int b) {
- if (edits.isEmpty()) {
- // Magic special case of an unmodified file.
- //
- return b;
- }
-
- for (int i = 0; i < edits.size(); i++) {
- final Edit e = edits.get(i);
- if (b < e.getBeginB()) {
- if (i == 0) {
- // Special case of context at start of file.
- //
- return b;
- }
- return e.getBeginA() - (e.getBeginB() - b);
- }
- if (e.getBeginB() <= b && b <= e.getEndB()) {
- return -1;
- }
- }
-
- final Edit last = edits.get(edits.size() - 1);
- return last.getEndA() + (b - last.getEndB());
- }
-
- private void packContent(boolean ignoredWhitespace) {
- EditList list = new EditList(edits, context, a.size(), b.size());
- for (final EditList.Hunk hunk : list.getHunks()) {
- while (hunk.next()) {
- if (hunk.isContextLine()) {
- final String lineA = a.src.getString(hunk.getCurA());
- a.dst.addLine(hunk.getCurA(), lineA);
-
- if (ignoredWhitespace) {
- // If we ignored whitespace in some form, also get the line
- // from b when it does not exactly match the line from a.
- //
- final String lineB = b.src.getString(hunk.getCurB());
- if (!lineA.equals(lineB)) {
- b.dst.addLine(hunk.getCurB(), lineB);
- }
- }
- hunk.incBoth();
- continue;
- }
-
- if (hunk.isDeletedA()) {
- a.addLine(hunk.getCurA());
- hunk.incA();
- }
-
- if (hunk.isInsertedB()) {
- b.addLine(hunk.getCurB());
- hunk.incB();
- }
- }
- }
- }
-
- private class Side {
- String path;
- ObjectId id;
- FileMode mode;
- byte[] srcContent;
- Text src;
- MimeType mimeType = MimeUtil2.UNKNOWN_MIME_TYPE;
- DisplayMethod displayMethod = DisplayMethod.DIFF;
- PatchScript.FileMode fileMode = PatchScript.FileMode.FILE;
- final SparseFileContent dst = new SparseFileContent();
-
- int size() {
- return src != null ? src.size() : 0;
- }
-
- void addLine(int line) {
- dst.addLine(line, src.getString(line));
- }
-
- void resolve(final Side other, final ObjectId within) throws IOException {
- try {
- final boolean reuse;
- if (Patch.COMMIT_MSG.equals(path)) {
- if (againstParent && (aId == within || within.equals(aId))) {
- id = ObjectId.zeroId();
- src = Text.EMPTY;
- srcContent = Text.NO_BYTES;
- mode = FileMode.MISSING;
- displayMethod = DisplayMethod.NONE;
- } else {
- id = within;
- src = Text.forCommit(db, reader, within);
- srcContent = src.getContent();
- if (src == Text.EMPTY) {
- mode = FileMode.MISSING;
- displayMethod = DisplayMethod.NONE;
- } else {
- mode = FileMode.REGULAR_FILE;
- }
- }
- reuse = false;
-
- } else {
- final TreeWalk tw = find(within);
-
- id = tw != null ? tw.getObjectId(0) : ObjectId.zeroId();
- mode = tw != null ? tw.getFileMode(0) : FileMode.MISSING;
- reuse = other != null && other.id.equals(id) && other.mode == mode;
-
- if (reuse) {
- srcContent = other.srcContent;
-
- } else if (mode.getObjectType() == Constants.OBJ_BLOB) {
- srcContent = Text.asByteArray(db.open(id, Constants.OBJ_BLOB));
-
- } else {
- srcContent = Text.NO_BYTES;
- }
-
- if (reuse) {
- mimeType = other.mimeType;
- displayMethod = other.displayMethod;
- src = other.src;
-
- } else if (srcContent.length > 0 && FileMode.SYMLINK != mode) {
- mimeType = registry.getMimeType(path, srcContent);
- if ("image".equals(mimeType.getMediaType())
- && registry.isSafeInline(mimeType)) {
- displayMethod = DisplayMethod.IMG;
- }
- }
- }
-
- if (mode == FileMode.MISSING) {
- displayMethod = DisplayMethod.NONE;
- }
-
- if (!reuse) {
- if (srcContent == Text.NO_BYTES) {
- src = Text.EMPTY;
- } else {
- src = new Text(srcContent);
- }
- }
-
- if (srcContent.length > 0 && srcContent[srcContent.length - 1] != '\n') {
- dst.setMissingNewlineAtEnd(true);
- }
- dst.setSize(size());
- dst.setPath(path);
-
- if (mode == FileMode.SYMLINK) {
- fileMode = PatchScript.FileMode.SYMLINK;
- } else if (mode == FileMode.GITLINK) {
- fileMode = PatchScript.FileMode.GITLINK;
- }
- } catch (IOException err) {
- throw new IOException("Cannot read " + within.name() + ":" + path, err);
- }
- }
-
- private TreeWalk find(final ObjectId within) throws MissingObjectException,
- IncorrectObjectTypeException, CorruptObjectException, IOException {
- if (path == null || within == null) {
- return null;
- }
- final RevWalk rw = new RevWalk(reader);
- final RevTree tree = rw.parseTree(within);
- return TreeWalk.forPath(reader, path, tree);
- }
- }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
deleted file mode 100644
index 797229c..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
+++ /dev/null
@@ -1,347 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.patch;
-
-import com.google.gerrit.common.data.CommentDetail;
-import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountInfoCacheFactory;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LargeObjectException;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListEntry;
-import com.google.gerrit.server.patch.PatchListKey;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import javax.annotation.Nullable;
-
-
-class PatchScriptFactory extends Handler<PatchScript> {
- interface Factory {
- PatchScriptFactory create(Patch.Key patchKey,
- @Assisted("patchSetA") PatchSet.Id patchSetA,
- @Assisted("patchSetB") PatchSet.Id patchSetB,
- AccountDiffPreference diffPrefs);
- }
-
- private static final Logger log =
- LoggerFactory.getLogger(PatchScriptFactory.class);
-
- private final GitRepositoryManager repoManager;
- private final Provider<PatchScriptBuilder> builderFactory;
- private final PatchListCache patchListCache;
- private final ReviewDb db;
- private final ChangeControl.Factory changeControlFactory;
- private final AccountInfoCacheFactory.Factory aicFactory;
-
- private final Patch.Key patchKey;
- @Nullable
- private final PatchSet.Id psa;
- private final PatchSet.Id psb;
- private final AccountDiffPreference diffPrefs;
-
- private final PatchSet.Id patchSetId;
- private final Change.Id changeId;
-
- private Change change;
- private PatchSet patchSet;
- private Project.NameKey projectKey;
- private ChangeControl control;
- private ObjectId aId;
- private ObjectId bId;
- private List<Patch> history;
- private CommentDetail comments;
-
- @Inject
- PatchScriptFactory(final GitRepositoryManager grm,
- Provider<PatchScriptBuilder> builderFactory,
- final PatchListCache patchListCache, final ReviewDb db,
- final ChangeControl.Factory changeControlFactory,
- final AccountInfoCacheFactory.Factory aicFactory,
- @Assisted final Patch.Key patchKey,
- @Assisted("patchSetA") @Nullable final PatchSet.Id patchSetA,
- @Assisted("patchSetB") final PatchSet.Id patchSetB,
- @Assisted final AccountDiffPreference diffPrefs) {
- this.repoManager = grm;
- this.builderFactory = builderFactory;
- this.patchListCache = patchListCache;
- this.db = db;
- this.changeControlFactory = changeControlFactory;
- this.aicFactory = aicFactory;
-
- this.patchKey = patchKey;
- this.psa = patchSetA;
- this.psb = patchSetB;
- this.diffPrefs = diffPrefs;
-
- patchSetId = patchKey.getParentKey();
- changeId = patchSetId.getParentKey();
- }
-
- @Override
- public PatchScript call() throws OrmException, NoSuchChangeException,
- LargeObjectException {
- validatePatchSetId(psa);
- validatePatchSetId(psb);
-
- control = changeControlFactory.validateFor(changeId);
- change = control.getChange();
- projectKey = change.getProject();
- patchSet = db.patchSets().get(patchSetId);
- if (patchSet == null) {
- throw new NoSuchChangeException(changeId);
- }
-
- aId = psa != null ? toObjectId(db, psa) : null;
- bId = toObjectId(db, psb);
-
- if ((psa != null && !control.isPatchVisible(db.patchSets().get(psa), db)) ||
- (psb != null && !control.isPatchVisible(db.patchSets().get(psb), db))) {
- throw new NoSuchChangeException(changeId);
- }
-
- final Repository git;
- try {
- git = repoManager.openRepository(projectKey);
- } catch (RepositoryNotFoundException e) {
- log.error("Repository " + projectKey + " not found", e);
- throw new NoSuchChangeException(changeId, e);
- } catch (IOException e) {
- log.error("Cannot open repository " + projectKey, e);
- throw new NoSuchChangeException(changeId, e);
- }
- try {
- final PatchList list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
- final PatchScriptBuilder b = newBuilder(list, git);
- final PatchListEntry content = list.get(patchKey.getFileName());
-
- loadCommentsAndHistory(content.getChangeType(), //
- content.getOldName(), //
- content.getNewName());
-
- return b.toPatchScript(content, comments, history);
- } catch (PatchListNotAvailableException e) {
- throw new NoSuchChangeException(changeId, e);
- } catch (IOException e) {
- log.error("File content unavailable", e);
- throw new NoSuchChangeException(changeId, e);
- } catch (org.eclipse.jgit.errors.LargeObjectException err) {
- throw new LargeObjectException("File content is too large", err);
- } finally {
- git.close();
- }
- }
-
- private PatchListKey keyFor(final Whitespace whitespace) {
- return new PatchListKey(projectKey, aId, bId, whitespace);
- }
-
- private PatchList listFor(final PatchListKey key)
- throws PatchListNotAvailableException {
- return patchListCache.get(key);
- }
-
- private PatchScriptBuilder newBuilder(final PatchList list, Repository git) {
- final AccountDiffPreference dp = new AccountDiffPreference(diffPrefs);
- final PatchScriptBuilder b = builderFactory.get();
- b.setRepository(git, projectKey);
- b.setChange(change);
- b.setDiffPrefs(dp);
- b.setTrees(list.isAgainstParent(), list.getOldId(), list.getNewId());
- return b;
- }
-
- private ObjectId toObjectId(final ReviewDb db, final PatchSet.Id psId)
- throws OrmException, NoSuchChangeException {
- if (!changeId.equals(psId.getParentKey())) {
- throw new NoSuchChangeException(changeId);
- }
-
- final PatchSet ps = db.patchSets().get(psId);
- if (ps == null || ps.getRevision() == null
- || ps.getRevision().get() == null) {
- throw new NoSuchChangeException(changeId);
- }
-
- try {
- return ObjectId.fromString(ps.getRevision().get());
- } catch (IllegalArgumentException e) {
- log.error("Patch set " + psId + " has invalid revision");
- throw new NoSuchChangeException(changeId, e);
- }
- }
-
- private void validatePatchSetId(final PatchSet.Id psId)
- throws NoSuchChangeException {
- if (psId == null) { // OK, means use base;
- } else if (changeId.equals(psId.getParentKey())) { // OK, same change;
- } else {
- throw new NoSuchChangeException(changeId);
- }
- }
-
- private void loadCommentsAndHistory(final ChangeType changeType,
- final String oldName, final String newName) throws OrmException {
- history = new ArrayList<Patch>();
- comments = new CommentDetail(psa, psb);
-
- final Map<Patch.Key, Patch> byKey = new HashMap<Patch.Key, Patch>();
- final AccountInfoCacheFactory aic = aicFactory.create();
-
- // This seems like a cheap trick. It doesn't properly account for a
- // file that gets renamed between patch set 1 and patch set 2. We
- // will wind up packing the wrong Patch object because we didn't do
- // proper rename detection between the patch sets.
- //
- for (final PatchSet ps : db.patchSets().byChange(changeId)) {
- if (!control.isPatchVisible(ps, db)) {
- continue;
- }
- String name = patchKey.get();
- if (psa != null) {
- switch (changeType) {
- case COPIED:
- case RENAMED:
- if (ps.getId().equals(psa)) {
- name = oldName;
- }
- break;
-
- case MODIFIED:
- case DELETED:
- case ADDED:
- case REWRITE:
- break;
- }
- }
-
- final Patch p = new Patch(new Patch.Key(ps.getId(), name));
- history.add(p);
- byKey.put(p.getKey(), p);
- }
-
- switch (changeType) {
- case ADDED:
- case MODIFIED:
- loadPublished(byKey, aic, newName);
- break;
-
- case DELETED:
- loadPublished(byKey, aic, newName);
- break;
-
- case COPIED:
- case RENAMED:
- if (psa != null) {
- loadPublished(byKey, aic, oldName);
- }
- loadPublished(byKey, aic, newName);
- break;
-
- case REWRITE:
- break;
- }
-
- final CurrentUser user = control.getCurrentUser();
- if (user instanceof IdentifiedUser) {
- final Account.Id me = ((IdentifiedUser) user).getAccountId();
- switch (changeType) {
- case ADDED:
- case MODIFIED:
- loadDrafts(byKey, aic, me, newName);
- break;
-
- case DELETED:
- loadDrafts(byKey, aic, me, newName);
- break;
-
- case COPIED:
- case RENAMED:
- if (psa != null) {
- loadDrafts(byKey, aic, me, oldName);
- }
- loadDrafts(byKey, aic, me, newName);
- break;
-
- case REWRITE:
- break;
- }
- }
-
- comments.setAccountInfoCache(aic.create());
- }
-
- private void loadPublished(final Map<Patch.Key, Patch> byKey,
- final AccountInfoCacheFactory aic, final String file) throws OrmException {
- for (PatchLineComment c : db.patchComments().publishedByChangeFile(changeId, file)) {
- if (comments.include(c)) {
- aic.want(c.getAuthor());
- }
-
- final Patch.Key pKey = c.getKey().getParentKey();
- final Patch p = byKey.get(pKey);
- if (p != null) {
- p.setCommentCount(p.getCommentCount() + 1);
- }
- }
- }
-
- private void loadDrafts(final Map<Patch.Key, Patch> byKey,
- final AccountInfoCacheFactory aic, final Account.Id me, final String file)
- throws OrmException {
- for (PatchLineComment c : db.patchComments().draftByChangeFileAuthor(changeId, file, me)) {
- if (comments.include(c)) {
- aic.want(me);
- }
-
- final Patch.Key pKey = c.getKey().getParentKey();
- final Patch p = byKey.get(pKey);
- if (p != null) {
- p.setDraftCount(p.getDraftCount() + 1);
- }
- }
- }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/SaveDraft.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/SaveDraft.java
index 18ab5ff5..66b5ec1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/SaveDraft.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/SaveDraft.java
@@ -84,12 +84,18 @@
throw new IllegalStateException("Parent comment must be on same side");
}
}
+ if (comment.getRange() != null
+ && comment.getLine() != comment.getRange().getEndLine()) {
+ throw new IllegalStateException(
+ "Range endLine must be on the same line as the comment");
+ }
final PatchLineComment nc =
new PatchLineComment(new PatchLineComment.Key(patchKey, ChangeUtil
.messageUUID(db)), comment.getLine(), me, comment.getParentUuid());
nc.setSide(comment.getSide());
nc.setMessage(comment.getMessage());
+ nc.setRange(comment.getRange());
db.patchComments().insert(Collections.singleton(nc));
db.commit();
return nc;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
deleted file mode 100644
index d26501e..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
+++ /dev/null
@@ -1,233 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.data.AddBranchResult;
-import com.google.gerrit.common.errors.InvalidRevisionException;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
-import com.google.gerrit.server.util.MagicBranch;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.ObjectWalk;
-import org.eclipse.jgit.revwalk.RevObject;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-
-class AddBranch extends Handler<AddBranchResult> {
- private static final Logger log = LoggerFactory.getLogger(AddBranch.class);
-
- interface Factory {
- AddBranch create(@Assisted Project.NameKey projectName,
- @Assisted("branchName") String branchName,
- @Assisted("startingRevision") String startingRevision);
- }
-
- private final ProjectControl.Factory projectControlFactory;
- private final ListBranches.Factory listBranchesFactory;
- private final IdentifiedUser identifiedUser;
- private final GitRepositoryManager repoManager;
- private final GitReferenceUpdated referenceUpdated;
- private final ChangeHooks hooks;
-
- private final Project.NameKey projectName;
- private final String branchName;
- private final String startingRevision;
-
- @Inject
- AddBranch(final ProjectControl.Factory projectControlFactory,
- final ListBranches.Factory listBranchesFactory,
- final IdentifiedUser identifiedUser,
- final GitRepositoryManager repoManager,
- GitReferenceUpdated referenceUpdated,
- final ChangeHooks hooks,
-
- @Assisted Project.NameKey projectName,
- @Assisted("branchName") String branchName,
- @Assisted("startingRevision") String startingRevision) {
- this.projectControlFactory = projectControlFactory;
- this.listBranchesFactory = listBranchesFactory;
- this.identifiedUser = identifiedUser;
- this.repoManager = repoManager;
- this.referenceUpdated = referenceUpdated;
- this.hooks = hooks;
-
- this.projectName = projectName;
- this.branchName = branchName;
- this.startingRevision = startingRevision;
- }
-
- @Override
- public AddBranchResult call() throws NoSuchProjectException, IOException {
- final ProjectControl projectControl =
- projectControlFactory.controlFor(projectName);
-
- String refname = branchName;
- while (refname.startsWith("/")) {
- refname = refname.substring(1);
- }
- if (!refname.startsWith(Constants.R_REFS)) {
- refname = Constants.R_HEADS + refname;
- }
- if (!Repository.isValidRefName(refname)) {
- return new AddBranchResult(new AddBranchResult.Error(
- AddBranchResult.Error.Type.INVALID_NAME, refname));
- }
- if (MagicBranch.isMagicBranch(refname)) {
- return new AddBranchResult(
- new AddBranchResult.Error(
- AddBranchResult.Error.Type.BRANCH_CREATION_NOT_ALLOWED_UNDER_REFNAME_PREFIX,
- MagicBranch.getMagicRefNamePrefix(refname)));
- }
-
- final Branch.NameKey name = new Branch.NameKey(projectName, refname);
- final RefControl refControl = projectControl.controlForRef(name);
- final Repository repo = repoManager.openRepository(projectName);
- try {
- final ObjectId revid = parseStartingRevision(repo);
- final RevWalk rw = verifyConnected(repo, revid);
- RevObject object = rw.parseAny(revid);
-
- if (refname.startsWith(Constants.R_HEADS)) {
- // Ensure that what we start the branch from is a commit. If we
- // were given a tag, deference to the commit instead.
- //
- try {
- object = rw.parseCommit(object);
- } catch (IncorrectObjectTypeException notCommit) {
- throw new IllegalStateException(startingRevision + " not a commit");
- }
- }
-
- if (!refControl.canCreate(rw, object)) {
- throw new IllegalStateException("Cannot create " + refname);
- }
-
- try {
- final RefUpdate u = repo.updateRef(refname);
- u.setExpectedOldObjectId(ObjectId.zeroId());
- u.setNewObjectId(object.copy());
- u.setRefLogIdent(identifiedUser.newRefLogIdent());
- u.setRefLogMessage("created via web from " + startingRevision, false);
- final RefUpdate.Result result = u.update(rw);
- switch (result) {
- case FAST_FORWARD:
- case NEW:
- case NO_CHANGE:
- referenceUpdated.fire(name.getParentKey(), u);
- hooks.doRefUpdatedHook(name, u, identifiedUser.getAccount());
- break;
- case LOCK_FAILURE:
- if (repo.getRef(refname) != null) {
- return new AddBranchResult(new AddBranchResult.Error(
- AddBranchResult.Error.Type.BRANCH_ALREADY_EXISTS, refname));
- }
- String refPrefix = getRefPrefix(refname);
- while (!Constants.R_HEADS.equals(refPrefix)) {
- if (repo.getRef(refPrefix) != null) {
- return new AddBranchResult(new AddBranchResult.Error(
- AddBranchResult.Error.Type.BRANCH_CREATION_CONFLICT, refPrefix));
- }
- refPrefix = getRefPrefix(refPrefix);
- }
- default: {
- throw new IOException(result.name());
- }
- }
- } catch (IOException err) {
- log.error("Cannot create branch " + name, err);
- throw err;
- }
- } catch (InvalidRevisionException e) {
- return new AddBranchResult(new AddBranchResult.Error(
- AddBranchResult.Error.Type.INVALID_REVISION));
- } finally {
- repo.close();
- }
-
- return new AddBranchResult(listBranchesFactory.create(projectName).call());
- }
-
- private static String getRefPrefix(final String refName) {
- final int i = refName.lastIndexOf('/');
- if (i > Constants.R_HEADS.length() - 1) {
- return refName.substring(0, i);
- }
- return Constants.R_HEADS;
- }
-
- private ObjectId parseStartingRevision(final Repository repo)
- throws InvalidRevisionException {
- try {
- final ObjectId revid = repo.resolve(startingRevision);
- if (revid == null) {
- throw new InvalidRevisionException();
- }
- return revid;
- } catch (IOException err) {
- log.error("Cannot resolve \"" + startingRevision + "\" in project \""
- + projectName + "\"", err);
- throw new InvalidRevisionException();
- }
- }
-
- private RevWalk verifyConnected(final Repository repo, final ObjectId revid)
- throws InvalidRevisionException {
- try {
- final ObjectWalk rw = new ObjectWalk(repo);
- try {
- rw.markStart(rw.parseCommit(revid));
- } catch (IncorrectObjectTypeException err) {
- throw new InvalidRevisionException();
- }
- for (final Ref r : repo.getAllRefs().values()) {
- try {
- rw.markUninteresting(rw.parseAny(r.getObjectId()));
- } catch (MissingObjectException err) {
- continue;
- }
- }
- rw.checkConnectivity();
- return rw;
- } catch (IncorrectObjectTypeException err) {
- throw new InvalidRevisionException();
- } catch (MissingObjectException err) {
- throw new InvalidRevisionException();
- } catch (IOException err) {
- log.error("Repository \"" + repo.getDirectory()
- + "\" may be corrupt; suggest running git fsck", err);
- throw new InvalidRevisionException();
- }
- }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
deleted file mode 100644
index 41354aa..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.PerRequestProjectControlCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.OrmConcurrencyException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-
-import java.io.IOException;
-
-class ChangeProjectSettings extends Handler<ProjectDetail> {
- interface Factory {
- ChangeProjectSettings create(@Assisted Project update);
- }
-
- private final ProjectDetailFactory.Factory projectDetailFactory;
- private final ProjectControl.Factory projectControlFactory;
- private final GitRepositoryManager mgr;
- private final MetaDataUpdate.User metaDataUpdateFactory;
- private final Provider<PerRequestProjectControlCache> userCache;
-
- private final Project update;
-
- @Inject
- ChangeProjectSettings(
- final ProjectDetailFactory.Factory projectDetailFactory,
- final ProjectControl.Factory projectControlFactory,
- final GitRepositoryManager mgr,
- final MetaDataUpdate.User metaDataUpdateFactory,
- final Provider<PerRequestProjectControlCache> uc,
- @Assisted final Project update) {
- this.projectDetailFactory = projectDetailFactory;
- this.projectControlFactory = projectControlFactory;
- this.mgr = mgr;
- this.userCache = uc;
- this.metaDataUpdateFactory = metaDataUpdateFactory;
-
- this.update = update;
- }
-
- @Override
- public ProjectDetail call() throws NoSuchProjectException, OrmException,
- IOException {
- final Project.NameKey projectName = update.getNameKey();
- projectControlFactory.ownerFor(projectName);
-
- final MetaDataUpdate md;
- try {
- md = metaDataUpdateFactory.create(projectName);
- } catch (RepositoryNotFoundException notFound) {
- throw new NoSuchProjectException(projectName);
- } catch (IOException e) {
- throw new OrmException(e);
- }
- try {
- // TODO We really should take advantage of the Git commit DAG and
- // ensure the current version matches the old version the caller read.
- //
- ProjectConfig config = ProjectConfig.read(md);
- config.getProject().copySettingsFrom(update);
-
- md.setMessage("Modified project settings\n");
- try {
- config.commit(md);
- mgr.setProjectDescription(projectName, update.getDescription());
- userCache.get().evict(config.getProject());
- } catch (IOException e) {
- throw new OrmConcurrencyException("Cannot update " + projectName);
- }
- } catch (ConfigInvalidException err) {
- throw new OrmException("Cannot read project " + projectName, err);
- } catch (IOException err) {
- throw new OrmException("Cannot update project " + projectName, err);
- } finally {
- md.close();
- }
-
- return projectDetailFactory.create(projectName).call();
- }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
deleted file mode 100644
index 8f5430d..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
+++ /dev/null
@@ -1,142 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Set;
-
-class DeleteBranches extends Handler<Set<Branch.NameKey>> {
- private static final Logger log =
- LoggerFactory.getLogger(DeleteBranches.class);
-
- interface Factory {
- DeleteBranches create(@Assisted Project.NameKey name,
- @Assisted Set<Branch.NameKey> toRemove);
- }
-
- private final ProjectControl.Factory projectControlFactory;
- private final GitRepositoryManager repoManager;
- private final GitReferenceUpdated gitRefUpdated;
- private final IdentifiedUser identifiedUser;
- private final ChangeHooks hooks;
- private final ReviewDb db;
-
- private final Project.NameKey projectName;
- private final Set<Branch.NameKey> toRemove;
-
- @Inject
- DeleteBranches(final ProjectControl.Factory projectControlFactory,
- final GitRepositoryManager repoManager,
- final GitReferenceUpdated gitRefUpdated,
- final IdentifiedUser identifiedUser,
- final ChangeHooks hooks,
- final ReviewDb db,
-
- @Assisted Project.NameKey name, @Assisted Set<Branch.NameKey> toRemove) {
- this.projectControlFactory = projectControlFactory;
- this.repoManager = repoManager;
- this.gitRefUpdated = gitRefUpdated;
- this.identifiedUser = identifiedUser;
- this.hooks = hooks;
- this.db = db;
-
- this.projectName = name;
- this.toRemove = toRemove;
- }
-
- @Override
- public Set<Branch.NameKey> call() throws NoSuchProjectException,
- RepositoryNotFoundException, OrmException, IOException {
- final ProjectControl projectControl =
- projectControlFactory.controlFor(projectName);
-
- final Iterator<Branch.NameKey> branchIt = toRemove.iterator();
- while (branchIt.hasNext()) {
- final Branch.NameKey k = branchIt.next();
- if (!projectName.equals(k.getParentKey())) {
- throw new IllegalArgumentException("All keys must be from same project");
- }
- if (!projectControl.controlForRef(k).canDelete()) {
- throw new IllegalStateException("Cannot delete " + k.getShortName());
- }
-
- if (db.changes().byBranchOpenAll(k).iterator().hasNext()) {
- branchIt.remove();
- }
- }
-
- final Set<Branch.NameKey> deleted = new HashSet<Branch.NameKey>();
- final Repository r = repoManager.openRepository(projectName);
- try {
- for (final Branch.NameKey branchKey : toRemove) {
- final String refname = branchKey.get();
- final RefUpdate.Result result;
- final RefUpdate u;
- try {
- u = r.updateRef(refname);
- u.setForceUpdate(true);
- result = u.delete();
- } catch (IOException e) {
- log.error("Cannot delete " + branchKey, e);
- continue;
- }
-
- switch (result) {
- case NEW:
- case NO_CHANGE:
- case FAST_FORWARD:
- case FORCED:
- deleted.add(branchKey);
- gitRefUpdated.fire(projectName, u);
- hooks.doRefUpdatedHook(branchKey, u, identifiedUser.getAccount());
- break;
-
- case REJECTED_CURRENT_BRANCH:
- log.warn("Cannot delete " + branchKey + ": " + result.name());
- break;
-
- default:
- log.error("Cannot delete " + branchKey + ": " + result.name());
- break;
- }
- }
- } finally {
- r.close();
- }
- return deleted;
- }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
deleted file mode 100644
index 2366423..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
+++ /dev/null
@@ -1,172 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.common.data.ListBranchesResult;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-class ListBranches extends Handler<ListBranchesResult> {
- interface Factory {
- ListBranches create(@Assisted Project.NameKey name);
- }
-
- private final ProjectControl.Factory projectControlFactory;
- private final GitRepositoryManager repoManager;
-
- private final Project.NameKey projectName;
-
- @Inject
- ListBranches(final ProjectControl.Factory projectControlFactory,
- final GitRepositoryManager repoManager,
-
- @Assisted final Project.NameKey name) {
- this.projectControlFactory = projectControlFactory;
- this.repoManager = repoManager;
-
- this.projectName = name;
- }
-
- @Override
- public ListBranchesResult call() throws NoSuchProjectException, IOException {
- final ProjectControl pctl = projectControlFactory.validateFor( //
- projectName, //
- ProjectControl.OWNER | ProjectControl.VISIBLE);
-
- final List<Branch> branches = new ArrayList<Branch>();
- Branch headBranch = null;
- Branch configBranch = null;
- final Set<String> targets = new HashSet<String>();
-
- final Repository db;
- try {
- db = repoManager.openRepository(projectName);
- } catch (RepositoryNotFoundException noGitRepository) {
- return new ListBranchesResult(branches, false, true);
- }
- try {
- final Map<String, Ref> all = db.getAllRefs();
-
- if (!all.containsKey(Constants.HEAD)) {
- // The branch pointed to by HEAD doesn't exist yet, so getAllRefs
- // filtered it out. If we ask for it individually we can find the
- // underlying target and put it into the map anyway.
- //
- try {
- Ref head = db.getRef(Constants.HEAD);
- if (head != null) {
- all.put(Constants.HEAD, head);
- }
- } catch (IOException e) {
- // Ignore the failure reading HEAD.
- }
- }
-
- for (final Ref ref : all.values()) {
- if (ref.isSymbolic()) {
- targets.add(ref.getTarget().getName());
- }
- }
-
- for (final Ref ref : all.values()) {
- if (ref.isSymbolic()) {
- // A symbolic reference to another branch, instead of
- // showing the resolved value, show the name it references.
- //
- String target = ref.getTarget().getName();
- RefControl targetRefControl = pctl.controlForRef(target);
- if (!targetRefControl.isVisible()) {
- continue;
- }
- if (target.startsWith(Constants.R_HEADS)) {
- target = target.substring(Constants.R_HEADS.length());
- }
-
- Branch b = createBranch(ref.getName());
- b.setRevision(new RevId(target));
-
- if (Constants.HEAD.equals(ref.getName())) {
- b.setCanDelete(false);
- headBranch = b;
- } else {
- b.setCanDelete(targetRefControl.canDelete());
- branches.add(b);
- }
- continue;
- }
-
- final RefControl refControl = pctl.controlForRef(ref.getName());
- if (refControl.isVisible()) {
- if (ref.getName().startsWith(Constants.R_HEADS)) {
- branches.add(createBranch(ref, refControl, targets));
- } else if (GitRepositoryManager.REF_CONFIG.equals(ref.getName())) {
- configBranch = createBranch(ref, refControl, targets);
- }
- }
- }
- } finally {
- db.close();
- }
- Collections.sort(branches, new Comparator<Branch>() {
- @Override
- public int compare(final Branch a, final Branch b) {
- return a.getName().compareTo(b.getName());
- }
- });
- if (configBranch != null) {
- branches.add(0, configBranch);
- }
- if (headBranch != null) {
- branches.add(0, headBranch);
- }
- return new ListBranchesResult(branches, pctl.canAddRefs(), false);
- }
-
- private Branch createBranch(final Ref ref, final RefControl refControl,
- final Set<String> targets) {
- final Branch b = createBranch(ref.getName());
- if (ref.getObjectId() != null) {
- b.setRevision(new RevId(ref.getObjectId().name()));
- }
- b.setCanDelete(!targets.contains(ref.getName()) && refControl.canDelete());
- return b;
- }
-
- private Branch createBranch(final String name) {
- return new Branch(new Branch.NameKey(projectName, name));
- }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
index 0e46bc3..66ba75c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
@@ -15,12 +15,8 @@
package com.google.gerrit.httpd.rpc.project;
import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.AddBranchResult;
-import com.google.gerrit.common.data.ListBranchesResult;
import com.google.gerrit.common.data.ProjectAccess;
import com.google.gerrit.common.data.ProjectAdminService;
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -29,49 +25,19 @@
import org.eclipse.jgit.lib.ObjectId;
import java.util.List;
-import java.util.Set;
class ProjectAdminServiceImpl implements ProjectAdminService {
- private final AddBranch.Factory addBranchFactory;
private final ChangeProjectAccess.Factory changeProjectAccessFactory;
private final ReviewProjectAccess.Factory reviewProjectAccessFactory;
- private final ChangeProjectSettings.Factory changeProjectSettingsFactory;
- private final DeleteBranches.Factory deleteBranchesFactory;
- private final ListBranches.Factory listBranchesFactory;
- private final VisibleProjectDetails.Factory visibleProjectDetailsFactory;
private final ProjectAccessFactory.Factory projectAccessFactory;
- private final ProjectDetailFactory.Factory projectDetailFactory;
@Inject
- ProjectAdminServiceImpl(final AddBranch.Factory addBranchFactory,
- final ChangeProjectAccess.Factory changeProjectAccessFactory,
+ ProjectAdminServiceImpl(final ChangeProjectAccess.Factory changeProjectAccessFactory,
final ReviewProjectAccess.Factory reviewProjectAccessFactory,
- final ChangeProjectSettings.Factory changeProjectSettingsFactory,
- final DeleteBranches.Factory deleteBranchesFactory,
- final ListBranches.Factory listBranchesFactory,
- final VisibleProjectDetails.Factory visibleProjectDetailsFactory,
- final ProjectAccessFactory.Factory projectAccessFactory,
- final ProjectDetailFactory.Factory projectDetailFactory) {
- this.addBranchFactory = addBranchFactory;
+ final ProjectAccessFactory.Factory projectAccessFactory) {
this.changeProjectAccessFactory = changeProjectAccessFactory;
this.reviewProjectAccessFactory = reviewProjectAccessFactory;
- this.changeProjectSettingsFactory = changeProjectSettingsFactory;
- this.deleteBranchesFactory = deleteBranchesFactory;
- this.listBranchesFactory = listBranchesFactory;
- this.visibleProjectDetailsFactory = visibleProjectDetailsFactory;
this.projectAccessFactory = projectAccessFactory;
- this.projectDetailFactory = projectDetailFactory;
- }
-
- @Override
- public void visibleProjectDetails(final AsyncCallback<List<ProjectDetail>> callback) {
- visibleProjectDetailsFactory.create().to(callback);
- }
-
- @Override
- public void projectDetail(final Project.NameKey projectName,
- final AsyncCallback<ProjectDetail> callback) {
- projectDetailFactory.create(projectName).to(callback);
}
@Override
@@ -80,12 +46,6 @@
projectAccessFactory.create(projectName).to(callback);
}
- @Override
- public void changeProjectSettings(final Project update,
- final AsyncCallback<ProjectDetail> callback) {
- changeProjectSettingsFactory.create(update).to(callback);
- }
-
private static ObjectId getBase(final String baseRevision) {
if (baseRevision != null && !baseRevision.isEmpty()) {
return ObjectId.fromString(baseRevision);
@@ -106,25 +66,4 @@
AsyncCallback<Change.Id> cb) {
reviewProjectAccessFactory.create(projectName, getBase(baseRevision), sections, msg).to(cb);
}
-
- @Override
- public void listBranches(final Project.NameKey projectName,
- final AsyncCallback<ListBranchesResult> callback) {
- listBranchesFactory.create(projectName).to(callback);
- }
-
- @Override
- public void deleteBranch(final Project.NameKey projectName,
- final Set<Branch.NameKey> toRemove,
- final AsyncCallback<Set<Branch.NameKey>> callback) {
- deleteBranchesFactory.create(projectName, toRemove).to(callback);
- }
-
- @Override
- public void addBranch(final Project.NameKey projectName,
- final String branchName, final String startingRevision,
- final AsyncCallback<AddBranchResult> callback) {
- addBranchFactory.create(projectName, branchName, startingRevision).to(
- callback);
- }
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
deleted file mode 100644
index 2533feb..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.common.collect.Iterables;
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.InheritedBoolean;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-
-import java.io.IOException;
-
-class ProjectDetailFactory extends Handler<ProjectDetail> {
- interface Factory {
- ProjectDetailFactory create(@Assisted Project.NameKey name);
- }
-
- private final ProjectControl.Factory projectControlFactory;
- private final GitRepositoryManager gitRepositoryManager;
-
- private final Project.NameKey projectName;
-
- @Inject
- ProjectDetailFactory(final ProjectControl.Factory projectControlFactory,
- final GitRepositoryManager gitRepositoryManager,
- @Assisted final Project.NameKey name) {
- this.projectControlFactory = projectControlFactory;
- this.gitRepositoryManager = gitRepositoryManager;
- this.projectName = name;
- }
-
- @Override
- public ProjectDetail call() throws NoSuchProjectException, IOException {
- final ProjectControl pc =
- projectControlFactory.validateFor(projectName, ProjectControl.OWNER
- | ProjectControl.VISIBLE);
- final ProjectState projectState = pc.getProjectState();
- final ProjectDetail detail = new ProjectDetail();
- detail.setProject(projectState.getProject());
-
- final boolean userIsOwner = pc.isOwner();
- final boolean userIsOwnerAnyRef = pc.isOwnerAnyRef();
-
- detail.setCanModifyAccess(userIsOwnerAnyRef);
- detail.setCanModifyAgreements(userIsOwner);
- detail.setCanModifyDescription(userIsOwner);
- detail.setCanModifyMergeType(userIsOwner);
- detail.setCanModifyState(userIsOwner);
-
- final InheritedBoolean useContributorAgreements = new InheritedBoolean();
- final InheritedBoolean useSignedOffBy = new InheritedBoolean();
- final InheritedBoolean useContentMerge = new InheritedBoolean();
- final InheritedBoolean requireChangeID = new InheritedBoolean();
- useContributorAgreements.setValue(projectState.getProject()
- .getUseContributorAgreements());
- useSignedOffBy.setValue(projectState.getProject().getUseSignedOffBy());
- useContentMerge.setValue(projectState.getProject().getUseContentMerge());
- requireChangeID.setValue(projectState.getProject().getRequireChangeID());
- ProjectState parentState = Iterables.getFirst(projectState.parents(), null);
- if (parentState != null) {
- useContributorAgreements.setInheritedValue(parentState
- .isUseContributorAgreements());
- useSignedOffBy.setInheritedValue(parentState.isUseSignedOffBy());
- useContentMerge.setInheritedValue(parentState.isUseContentMerge());
- requireChangeID.setInheritedValue(parentState.isRequireChangeID());
- }
- detail.setUseContributorAgreements(useContributorAgreements);
- detail.setUseSignedOffBy(useSignedOffBy);
- detail.setUseContentMerge(useContentMerge);
- detail.setRequireChangeID(requireChangeID);
-
- final Project.NameKey projectName = projectState.getProject().getNameKey();
- Repository git;
- try {
- git = gitRepositoryManager.openRepository(projectName);
- } catch (RepositoryNotFoundException err) {
- throw new NoSuchProjectException(projectName);
- }
- try {
- Ref head = git.getRef(Constants.HEAD);
- if (head != null && head.isSymbolic()
- && GitRepositoryManager.REF_CONFIG.equals(head.getLeaf().getName())) {
- detail.setPermissionOnly(true);
- }
- } catch (IOException err) {
- throw new NoSuchProjectException(projectName);
- } finally {
- git.close();
- }
-
- return detail;
- }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
index 2d4f210..bd5f940 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
@@ -28,15 +28,9 @@
install(new FactoryModule() {
@Override
protected void configure() {
- factory(AddBranch.Factory.class);
factory(ChangeProjectAccess.Factory.class);
factory(ReviewProjectAccess.Factory.class);
- factory(ChangeProjectSettings.Factory.class);
- factory(DeleteBranches.Factory.class);
- factory(ListBranches.Factory.class);
- factory(VisibleProjectDetails.Factory.class);
factory(ProjectAccessFactory.Factory.class);
- factory(ProjectDetailFactory.Factory.class);
}
});
rpc(ProjectAdminServiceImpl.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index 251249f..e209e29 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -32,6 +32,7 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectControl;
@@ -63,17 +64,19 @@
private final PatchSetInfoFactory patchSetInfoFactory;
private final Provider<PostReviewers> reviewersProvider;
private final ChangeControl.GenericFactory changeFactory;
+ private final ChangeIndexer indexer;
@Inject
ReviewProjectAccess(final ProjectControl.Factory projectControlFactory,
- final GroupBackend groupBackend,
- final MetaDataUpdate.User metaDataUpdateFactory, final ReviewDb db,
- final IdentifiedUser user, final PatchSetInfoFactory patchSetInfoFactory,
- final Provider<PostReviewers> reviewersProvider,
- final ChangeControl.GenericFactory changeFactory,
+ GroupBackend groupBackend,
+ MetaDataUpdate.User metaDataUpdateFactory, ReviewDb db,
+ IdentifiedUser user, PatchSetInfoFactory patchSetInfoFactory,
+ Provider<PostReviewers> reviewersProvider,
+ ChangeControl.GenericFactory changeFactory,
+ ChangeIndexer indexer,
- @Assisted final Project.NameKey projectName,
- @Nullable @Assisted final ObjectId base,
+ @Assisted Project.NameKey projectName,
+ @Nullable @Assisted ObjectId base,
@Assisted List<AccessSection> sectionList,
@Nullable @Assisted String message) {
super(projectControlFactory, groupBackend, metaDataUpdateFactory,
@@ -83,6 +86,7 @@
this.patchSetInfoFactory = patchSetInfoFactory;
this.reviewersProvider = reviewersProvider;
this.changeFactory = changeFactory;
+ this.indexer = indexer;
}
@Override
@@ -122,6 +126,7 @@
} finally {
db.rollback();
}
+ indexer.index(change);
return changeId;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
deleted file mode 100644
index 1c22d83..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-class VisibleProjectDetails extends Handler<List<ProjectDetail>> {
-
- interface Factory {
- VisibleProjectDetails create();
- }
-
- private final ProjectCache projectCache;
- private final ProjectDetailFactory.Factory projectDetailFactory;
-
- @Inject
- VisibleProjectDetails(final ProjectCache projectCache,
- final ProjectDetailFactory.Factory projectDetailFactory) {
- this.projectCache = projectCache;
- this.projectDetailFactory = projectDetailFactory;
- }
-
- @Override
- public List<ProjectDetail> call() {
- List<ProjectDetail> result = new ArrayList<ProjectDetail>();
- for (Project.NameKey projectName : projectCache.all()) {
- try {
- result.add(projectDetailFactory.create(projectName).call());
- } catch (NoSuchProjectException e) {
- } catch (IOException e) {
- }
- }
- Collections.sort(result, new Comparator<ProjectDetail>() {
- public int compare(final ProjectDetail a, final ProjectDetail b) {
- return a.project.getName().compareTo(b.project.getName());
- }
- });
- return result;
- }
-}
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
index ce100a5..9f3fa1e 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
@@ -44,9 +44,9 @@
</noscript>
</div>
<div id="gerrit_body"></div>
- <div style="clear: both; margin-top: 15px; padding-top: 2px; margin-bottom: 15px;">
+ <div style="clear: both">
<div id="gerrit_footer"></div>
- <div id="gerrit_btmmenu" style="clear: both;"></div>
+ <div id="gerrit_btmmenu"></div>
</div>
<iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
<script id="gerrit_module"></script>
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java
deleted file mode 100644
index 5cfde6a..0000000
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java
+++ /dev/null
@@ -1,374 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import static org.easymock.EasyMock.createStrictMock;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.expectLastCall;
-import static org.easymock.EasyMock.replay;
-import static org.easymock.EasyMock.verify;
-import static org.eclipse.jgit.lib.Constants.HEAD;
-import static org.eclipse.jgit.lib.Constants.R_HEADS;
-import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import com.google.gerrit.common.data.ListBranchesResult;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
-import com.google.gwtorm.client.KeyUtil;
-import com.google.gwtorm.server.StandardKeyEncoder;
-
-import org.easymock.IExpectationSetters;
-import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectIdRef;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.SymbolicRef;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class ListBranchesTest extends LocalDiskRepositoryTestCase {
- static {
- KeyUtil.setEncoderImpl(new StandardKeyEncoder());
- }
-
- private ObjectId idA;
- private Project.NameKey name;
- private Repository realDb;
- private Repository mockDb;
- private ProjectControl.Factory pcf;
- private ProjectControl pc;
- private GitRepositoryManager grm;
- private List<RefControl> refMocks;
-
- @Override
- @Before
- public void setUp() throws Exception {
- super.setUp();
-
- idA = ObjectId.fromString("df84c2f4f7ce7e0b25cdeac84b8870bcff319885");
- name = new Project.NameKey("test");
- realDb = createBareRepository();
-
- mockDb = createStrictMock(Repository.class);
- pc = createStrictMock(ProjectControl.class);
- pcf = createStrictMock(ProjectControl.Factory.class);
- grm = createStrictMock(GitRepositoryManager.class);
- refMocks = new ArrayList<RefControl>();
- }
-
- private IExpectationSetters<ProjectControl> validate()
- throws NoSuchProjectException {
- return expect(pcf.validateFor(eq(name), //
- eq(ProjectControl.OWNER | ProjectControl.VISIBLE)));
- }
-
- private void doReplay() {
- replay(mockDb, pc, pcf, grm);
- replay(refMocks.toArray());
- }
-
- private void doVerify() {
- verify(mockDb, pc, pcf, grm);
- verify(refMocks.toArray());
- }
-
- private void set(String branch, ObjectId id) throws IOException {
- final RefUpdate u = realDb.updateRef(R_HEADS + branch);
- u.setForceUpdate(true);
- u.setNewObjectId(id);
- switch (u.update()) {
- case NEW:
- case FAST_FORWARD:
- case FORCED:
- break;
- default:
- fail("unexpected update failure " + branch + " " + u.getResult());
- }
- }
-
- @Test
- public void testProjectNotVisible() throws Exception {
- final NoSuchProjectException err = new NoSuchProjectException(name);
- validate().andThrow(err);
- doReplay();
- try {
- new ListBranches(pcf, grm, name).call();
- fail("did not throw when expected not authorized");
- } catch (NoSuchProjectException e2) {
- assertSame(err, e2);
- }
- doVerify();
- }
-
-
- private ListBranchesResult permitted(boolean getHead)
- throws NoSuchProjectException, IOException {
- Map<String, Ref> refs = realDb.getAllRefs();
-
- validate().andReturn(pc);
-
- expect(grm.openRepository(eq(name))).andReturn(mockDb);
- expect(mockDb.getAllRefs()).andDelegateTo(realDb);
- if (getHead) {
- expect(mockDb.getRef(HEAD)).andDelegateTo(realDb);
- if (!refs.containsKey(HEAD) && realDb.getRef(HEAD) != null) {
- refs.put(HEAD, realDb.getRef(HEAD));
- }
- }
-
- Set<String> targets = targets(refs);
- for (Ref ref : refs.values()) {
- assumeVisible(ref, true, targets);
- }
-
- mockDb.close();
-
- expect(pc.canAddRefs()).andReturn(true);
-
- expectLastCall();
-
- doReplay();
- final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
- doVerify();
- assertNotNull(r);
- assertNotNull(r.getBranches());
- return r;
- }
-
- private Set<String> targets(Map<String, Ref> refs) {
- Set<String> targets = new HashSet<String>();
- for (Ref ref : refs.values()) {
- if (ref.isSymbolic()) {
- targets.add(ref.getLeaf().getName());
- }
- }
- return targets;
- }
-
- private void assumeVisible(Ref ref, boolean visible, Set<String> targets) {
- RefControl rc = createStrictMock(RefControl.class);
- refMocks.add(rc);
- expect(rc.isVisible()).andReturn(visible);
- if (visible && !ref.isSymbolic() && !targets.contains(ref.getName())) {
- expect(rc.canDelete()).andReturn(true);
- }
-
- if (ref.isSymbolic()) {
- expect(pc.controlForRef(ref.getTarget().getName())).andReturn(rc);
- } else {
- expect(pc.controlForRef(ref.getName())).andReturn(rc);
- }
- }
-
- @Test
- public void testEmptyProject() throws Exception {
- ListBranchesResult r = permitted(true);
-
- assertEquals(1, r.getBranches().size());
-
- Branch b = r.getBranches().get(0);
- assertNotNull(b);
-
- assertNotNull(b.getNameKey());
- assertSame(name, b.getNameKey().getParentKey());
- assertEquals(HEAD, b.getNameKey().get());
-
- assertEquals(HEAD, b.getName());
- assertEquals(HEAD, b.getShortName());
-
- assertNotNull(b.getRevision());
- assertEquals("master", b.getRevision().get());
- }
-
- @Test
- public void testMasterBranch() throws Exception {
- set("master", idA);
-
- ListBranchesResult r = permitted(false);
- assertEquals(2, r.getBranches().size());
-
- Branch b = r.getBranches().get(0);
- assertNotNull(b);
-
- assertNotNull(b.getNameKey());
- assertSame(name, b.getNameKey().getParentKey());
- assertEquals(HEAD, b.getNameKey().get());
-
- assertEquals(HEAD, b.getName());
- assertEquals(HEAD, b.getShortName());
-
- assertNotNull(b.getRevision());
- assertEquals("master", b.getRevision().get());
-
- b = r.getBranches().get(1);
- assertNotNull(b);
-
- assertNotNull(b.getNameKey());
- assertSame(name, b.getNameKey().getParentKey());
- assertEquals(R_HEADS + "master", b.getNameKey().get());
-
- assertEquals(R_HEADS + "master", b.getName());
- assertEquals("master", b.getShortName());
-
- assertNotNull(b.getRevision());
- assertEquals(idA.name(), b.getRevision().get());
- }
-
- @Test
- public void testBranchNotHead() throws Exception {
- set("foo", idA);
-
- ListBranchesResult r = permitted(true);
- assertEquals(2, r.getBranches().size());
-
- Branch b = r.getBranches().get(0);
- assertNotNull(b);
-
- assertNotNull(b.getNameKey());
- assertSame(name, b.getNameKey().getParentKey());
- assertEquals(HEAD, b.getNameKey().get());
-
- assertEquals(HEAD, b.getName());
- assertEquals(HEAD, b.getShortName());
-
- assertNotNull(b.getRevision());
- assertEquals("master", b.getRevision().get());
- assertFalse(b.getCanDelete());
-
- b = r.getBranches().get(1);
- assertNotNull(b);
-
- assertNotNull(b.getNameKey());
- assertSame(name, b.getNameKey().getParentKey());
- assertEquals(R_HEADS + "foo", b.getNameKey().get());
-
- assertEquals(R_HEADS + "foo", b.getName());
- assertEquals("foo", b.getShortName());
-
- assertNotNull(b.getRevision());
- assertEquals(idA.name(), b.getRevision().get());
- assertTrue(b.getCanDelete());
- }
-
- @Test
- public void testSortByName() throws Exception {
- Map<String, Ref> u = new LinkedHashMap<String, Ref>();
- u.put("foo", new ObjectIdRef.Unpeeled(LOOSE, R_HEADS + "foo", idA));
- u.put("bar", new ObjectIdRef.Unpeeled(LOOSE, R_HEADS + "bar", idA));
- u.put(HEAD, new SymbolicRef(HEAD, new ObjectIdRef.Unpeeled(LOOSE, R_HEADS
- + "master", null)));
-
- validate().andReturn(pc);
- expect(grm.openRepository(eq(name))).andReturn(mockDb);
- expect(mockDb.getAllRefs()).andReturn(u);
- for (Ref ref : u.values()) {
- assumeVisible(ref, true, targets(u));
- }
- expect(pc.canAddRefs()).andReturn(true);
- mockDb.close();
- expectLastCall();
-
- doReplay();
- final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
- doVerify();
- assertNotNull(r);
-
- assertEquals(3, r.getBranches().size());
- assertEquals(HEAD, r.getBranches().get(0).getShortName());
- assertEquals("bar", r.getBranches().get(1).getShortName());
- assertEquals("foo", r.getBranches().get(2).getShortName());
- }
-
- @Test
- public void testHeadNotVisible() throws Exception {
- ObjectIdRef.Unpeeled bar =
- new ObjectIdRef.Unpeeled(LOOSE, R_HEADS + "bar", idA);
- Map<String, Ref> u = new LinkedHashMap<String, Ref>();
- u.put(bar.getName(), bar);
- u.put(HEAD, new SymbolicRef(HEAD, bar));
-
- validate().andReturn(pc);
- expect(grm.openRepository(eq(name))).andReturn(mockDb);
- expect(mockDb.getAllRefs()).andReturn(u);
- assumeVisible(bar, false, targets(u));
- assumeVisible(bar, false, targets(u));
- expect(pc.canAddRefs()).andReturn(true);
- mockDb.close();
- expectLastCall();
-
- doReplay();
- final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
- doVerify();
- assertNotNull(r);
- assertTrue(r.getBranches().isEmpty());
- }
-
- @Test
- public void testHeadVisibleButBranchHidden() throws Exception {
- ObjectIdRef.Unpeeled bar =
- new ObjectIdRef.Unpeeled(LOOSE, R_HEADS + "bar", idA);
- ObjectIdRef.Unpeeled foo =
- new ObjectIdRef.Unpeeled(LOOSE, R_HEADS + "foo", idA);
-
- Map<String, Ref> u = new LinkedHashMap<String, Ref>();
- u.put(bar.getName(), bar);
- u.put(HEAD, new SymbolicRef(HEAD, bar));
- u.put(foo.getName(), foo);
-
- validate().andReturn(pc);
- expect(grm.openRepository(eq(name))).andReturn(mockDb);
- expect(mockDb.getAllRefs()).andReturn(u);
- assumeVisible(bar, true, targets(u));
- assumeVisible(bar, true, targets(u));
- assumeVisible(foo, false, targets(u));
- expect(pc.canAddRefs()).andReturn(true);
- mockDb.close();
- expectLastCall();
-
- doReplay();
- final ListBranchesResult r = new ListBranches(pcf, grm, name).call();
- doVerify();
- assertNotNull(r);
-
- assertEquals(2, r.getBranches().size());
-
- assertEquals(HEAD, r.getBranches().get(0).getShortName());
- assertFalse(r.getBranches().get(0).getCanDelete());
-
- assertEquals("bar", r.getBranches().get(1).getShortName());
- assertFalse(r.getBranches().get(1).getCanDelete());
- }
-}
diff --git a/gerrit-launcher/BUCK b/gerrit-launcher/BUCK
new file mode 100644
index 0000000..e5ff3d0
--- /dev/null
+++ b/gerrit-launcher/BUCK
@@ -0,0 +1,9 @@
+java_library(
+ name = 'launcher',
+ srcs = glob(['src/main/java/**/*.java']),
+ visibility = [
+ '//gerrit-acceptance-tests/...',
+ '//gerrit-main:main_lib',
+ '//gerrit-pgm:',
+ ],
+)
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
deleted file mode 100644
index e04acca..0000000
--- a/gerrit-launcher/pom.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-launcher</artifactId>
- <name>Gerrit Code Review - Launcher</name>
-
- <description>
- Bootstraps the rest of our classpath after Main
- </description>
-</project>
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index d49c6c7..afe61d2 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -299,7 +299,7 @@
return name;
}
- private static File myArchive;
+ private volatile static File myArchive;
/**
* Locate the JAR/WAR file we were launched from.
diff --git a/gerrit-lucene/BUCK b/gerrit-lucene/BUCK
new file mode 100644
index 0000000..b5e5d07
--- /dev/null
+++ b/gerrit-lucene/BUCK
@@ -0,0 +1,39 @@
+QUERY_BUILDER = [
+ 'src/main/java/com/google/gerrit/lucene/QueryBuilder.java',
+]
+
+java_library(
+ name = 'query_builder',
+ srcs = QUERY_BUILDER,
+ deps = [
+ '//gerrit-antlr:query_exception',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//lib:gwtorm',
+ '//lib:guava',
+ '//lib/lucene:core',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+java_library(
+ name = 'lucene',
+ srcs = glob(['src/main/java/**/*.java'], excludes = QUERY_BUILDER),
+ deps = [
+ ':query_builder',
+ '//gerrit-antlr:query_exception',
+ '//gerrit-extension-api:api',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//lib:guava',
+ '//lib:gwtorm',
+ '//lib:jsr305',
+ '//lib/guice:guice',
+ '//lib/guice:guice-assistedinject',
+ '//lib/jgit:jgit',
+ '//lib/log:api',
+ '//lib/lucene:analyzers-common',
+ '//lib/lucene:core',
+ ],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
new file mode 100644
index 0000000..c98bd5d
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -0,0 +1,433 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.lucene;
+
+import static com.google.gerrit.server.index.IndexRewriteImpl.CLOSED_STATUSES;
+import static com.google.gerrit.server.index.IndexRewriteImpl.OPEN_STATUSES;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.ChangeField.ChangeProtoField;
+import com.google.gerrit.server.index.ChangeField.PatchSetApprovalProtoField;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
+import com.google.gerrit.server.index.FieldType;
+import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.IndexRewriteImpl;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.analysis.util.CharArraySet;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.Field.Store;
+import org.apache.lucene.document.IntField;
+import org.apache.lucene.document.LongField;
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.document.TextField;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.IndexWriterConfig.OpenMode;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.SearcherManager;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.Version;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import javax.annotation.Nullable;
+
+/**
+ * Secondary index implementation using Apache Lucene.
+ * <p>
+ * Writes are managed using a single {@link IndexWriter} per process, committed
+ * aggressively. Reads use {@link SearcherManager} and periodically refresh,
+ * though there may be some lag between a committed write and it showing up to
+ * other threads' searchers.
+ */
+public class LuceneChangeIndex implements ChangeIndex {
+ private static final Logger log =
+ LoggerFactory.getLogger(LuceneChangeIndex.class);
+
+ public static final Version LUCENE_VERSION = Version.LUCENE_43;
+ public static final String CHANGES_OPEN = "open";
+ public static final String CHANGES_CLOSED = "closed";
+ private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
+ private static final String CHANGE_FIELD = ChangeField.CHANGE.getName();
+ private static final String APPROVAL_FIELD = ChangeField.APPROVAL.getName();
+
+ static interface Factory {
+ LuceneChangeIndex create(Schema<ChangeData> schema, String base);
+ }
+
+ private static IndexWriterConfig getIndexWriterConfig(Config cfg, String name) {
+ IndexWriterConfig writerConfig = new IndexWriterConfig(LUCENE_VERSION,
+ new StandardAnalyzer(LUCENE_VERSION, CharArraySet.EMPTY_SET));
+ writerConfig.setOpenMode(OpenMode.CREATE_OR_APPEND);
+ double m = 1 << 20;
+ writerConfig.setRAMBufferSizeMB(cfg.getLong("index", name, "ramBufferSize",
+ (long) (IndexWriterConfig.DEFAULT_RAM_BUFFER_SIZE_MB * m)) / m);
+ writerConfig.setMaxBufferedDocs(cfg.getInt("index", name, "maxBufferedDocs",
+ IndexWriterConfig.DEFAULT_MAX_BUFFERED_DOCS));
+ return writerConfig;
+ }
+
+ private final SitePaths sitePaths;
+ private final FillArgs fillArgs;
+ private final ExecutorService executor;
+ private final File dir;
+ private final Schema<ChangeData> schema;
+ private final SubIndex openIndex;
+ private final SubIndex closedIndex;
+
+ @AssistedInject
+ LuceneChangeIndex(
+ @GerritServerConfig Config cfg,
+ SitePaths sitePaths,
+ @IndexExecutor ListeningScheduledExecutorService executor,
+ FillArgs fillArgs,
+ @Assisted Schema<ChangeData> schema,
+ @Assisted @Nullable String base) throws IOException {
+ this.sitePaths = sitePaths;
+ this.fillArgs = fillArgs;
+ this.executor = executor;
+ this.schema = schema;
+
+ if (base == null) {
+ dir = LuceneVersionManager.getDir(sitePaths, schema);
+ } else {
+ dir = new File(base);
+ }
+ openIndex = new SubIndex(new File(dir, CHANGES_OPEN),
+ getIndexWriterConfig(cfg, "changes_open"));
+ closedIndex = new SubIndex(new File(dir, CHANGES_CLOSED),
+ getIndexWriterConfig(cfg, "changes_closed"));
+ }
+
+ @Override
+ public void close() {
+ List<Future<?>> closeFutures = Lists.newArrayListWithCapacity(2);
+ closeFutures.add(executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ openIndex.close();
+ }
+ }));
+ closeFutures.add(executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ closedIndex.close();
+ }
+ }));
+ for (Future<?> future : closeFutures) {
+ Futures.getUnchecked(future);
+ }
+ }
+
+ @Override
+ public Schema<ChangeData> getSchema() {
+ return schema;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public ListenableFuture<Void> insert(ChangeData cd) throws IOException {
+ Term id = QueryBuilder.idTerm(cd);
+ Document doc = toDocument(cd);
+ if (cd.getChange().getStatus().isOpen()) {
+ return allOf(
+ closedIndex.delete(id),
+ openIndex.insert(doc));
+ } else {
+ return allOf(
+ openIndex.delete(id),
+ closedIndex.insert(doc));
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public ListenableFuture<Void> replace(ChangeData cd) throws IOException {
+ Term id = QueryBuilder.idTerm(cd);
+ Document doc = toDocument(cd);
+ if (cd.getChange().getStatus().isOpen()) {
+ return allOf(
+ closedIndex.delete(id),
+ openIndex.replace(id, doc));
+ } else {
+ return allOf(
+ openIndex.delete(id),
+ closedIndex.replace(id, doc));
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public ListenableFuture<Void> delete(ChangeData cd) throws IOException {
+ Term id = QueryBuilder.idTerm(cd);
+ return allOf(
+ openIndex.delete(id),
+ closedIndex.delete(id));
+ }
+
+ private static <V> ListenableFuture<Void> allOf(ListenableFuture<V>... f) {
+ return Futures.transform(
+ Futures.allAsList(f),
+ new Function<List<V>, Void>() {
+ @Override
+ public Void apply(List<V> input) {
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public void deleteAll() throws IOException {
+ openIndex.deleteAll();
+ closedIndex.deleteAll();
+ }
+
+ @Override
+ public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
+ throws QueryParseException {
+ Set<Change.Status> statuses = IndexRewriteImpl.getPossibleStatus(p);
+ List<SubIndex> indexes = Lists.newArrayListWithCapacity(2);
+ if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
+ indexes.add(openIndex);
+ }
+ if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
+ indexes.add(closedIndex);
+ }
+ return new QuerySource(indexes, QueryBuilder.toQuery(p), limit);
+ }
+
+ @Override
+ public void markReady(boolean ready) throws IOException {
+ try {
+ FileBasedConfig cfg = LuceneVersionManager.loadGerritIndexConfig(sitePaths);
+ LuceneVersionManager.setReady(cfg, schema.getVersion(), ready);
+ cfg.save();
+ } catch (ConfigInvalidException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private static class QuerySource implements ChangeDataSource {
+ private static final ImmutableSet<String> FIELDS =
+ ImmutableSet.of(ID_FIELD, CHANGE_FIELD, APPROVAL_FIELD);
+
+ private final List<SubIndex> indexes;
+ private final Query query;
+ private final int limit;
+
+ public QuerySource(List<SubIndex> indexes, Query query, int limit) {
+ this.indexes = indexes;
+ this.query = query;
+ this.limit = limit;
+ }
+
+ @Override
+ public int getCardinality() {
+ return 10; // TODO(dborowitz): estimate from Lucene?
+ }
+
+ @Override
+ public boolean hasChange() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return query.toString();
+ }
+
+ @Override
+ public ResultSet<ChangeData> read() throws OrmException {
+ IndexSearcher[] searchers = new IndexSearcher[indexes.size()];
+ Sort sort = new Sort(
+ new SortField(
+ ChangeField.UPDATED.getName(),
+ SortField.Type.INT,
+ true /* descending */));
+ try {
+ TopDocs[] hits = new TopDocs[indexes.size()];
+ for (int i = 0; i < indexes.size(); i++) {
+ searchers[i] = indexes.get(i).acquire();
+ hits[i] = searchers[i].search(query, limit, sort);
+ }
+ TopDocs docs = TopDocs.merge(sort, limit, hits);
+
+ List<ChangeData> result =
+ Lists.newArrayListWithCapacity(docs.scoreDocs.length);
+ for (ScoreDoc sd : docs.scoreDocs) {
+ Document doc = searchers[sd.shardIndex].doc(sd.doc, FIELDS);
+ result.add(toChangeData(doc));
+ }
+
+ final List<ChangeData> r = Collections.unmodifiableList(result);
+ return new ResultSet<ChangeData>() {
+ @Override
+ public Iterator<ChangeData> iterator() {
+ return r.iterator();
+ }
+
+ @Override
+ public List<ChangeData> toList() {
+ return r;
+ }
+
+ @Override
+ public void close() {
+ // Do nothing.
+ }
+ };
+ } catch (IOException e) {
+ throw new OrmException(e);
+ } finally {
+ for (int i = 0; i < indexes.size(); i++) {
+ if (searchers[i] != null) {
+ try {
+ indexes.get(i).release(searchers[i]);
+ } catch (IOException e) {
+ log.warn("cannot release Lucene searcher", e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static ChangeData toChangeData(Document doc) {
+ BytesRef cb = doc.getBinaryValue(CHANGE_FIELD);
+ if (cb == null) {
+ int id = doc.getField(ID_FIELD).numericValue().intValue();
+ return new ChangeData(new Change.Id(id));
+ }
+
+ Change change = ChangeProtoField.CODEC.decode(
+ cb.bytes, cb.offset, cb.length);
+ ChangeData cd = new ChangeData(change);
+
+ BytesRef[] approvalsBytes = doc.getBinaryValues(APPROVAL_FIELD);
+ if (approvalsBytes != null) {
+ List<PatchSetApproval> approvals =
+ Lists.newArrayListWithCapacity(approvalsBytes.length);
+ for (BytesRef ab : approvalsBytes) {
+ approvals.add(PatchSetApprovalProtoField.CODEC.decode(
+ ab.bytes, ab.offset, ab.length));
+ }
+ cd.setCurrentApprovals(approvals);
+ }
+ return cd;
+ }
+
+ private Document toDocument(ChangeData cd) throws IOException {
+ try {
+ Document result = new Document();
+ for (FieldDef<ChangeData, ?> f : schema.getFields().values()) {
+ if (f.isRepeatable()) {
+ add(result, f, (Iterable<?>) f.get(cd, fillArgs));
+ } else {
+ Object val = f.get(cd, fillArgs);
+ if (val != null) {
+ add(result, f, Collections.singleton(val));
+ }
+ }
+ }
+ return result;
+ } catch (OrmException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private void add(Document doc, FieldDef<ChangeData, ?> f,
+ Iterable<?> values) throws OrmException {
+ String name = f.getName();
+ Store store = store(f);
+
+ if (f.getType() == FieldType.INTEGER) {
+ for (Object value : values) {
+ doc.add(new IntField(name, (Integer) value, store));
+ }
+ } else if (f.getType() == FieldType.LONG) {
+ for (Object value : values) {
+ doc.add(new LongField(name, (Long) value, store));
+ }
+ } else if (f.getType() == FieldType.TIMESTAMP) {
+ for (Object v : values) {
+ int t = QueryBuilder.toIndexTime((Timestamp) v);
+ doc.add(new IntField(name, t, store));
+ }
+ } else if (f.getType() == FieldType.EXACT
+ || f.getType() == FieldType.PREFIX) {
+ for (Object value : values) {
+ doc.add(new StringField(name, (String) value, store));
+ }
+ } else if (f.getType() == FieldType.FULL_TEXT) {
+ for (Object value : values) {
+ doc.add(new TextField(name, (String) value, store));
+ }
+ } else if (f.getType() == FieldType.STORED_ONLY) {
+ for (Object value : values) {
+ doc.add(new StoredField(name, (byte[]) value));
+ }
+ } else {
+ throw QueryBuilder.badFieldType(f.getType());
+ }
+ }
+
+ private static Field.Store store(FieldDef<?, ?> f) {
+ return f.isStored() ? Field.Store.YES : Field.Store.NO;
+ }
+}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
new file mode 100644
index 0000000..4e8f3f9
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -0,0 +1,115 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.lucene;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+public class LuceneIndexModule extends LifecycleModule {
+ private final Integer singleVersion;
+ private final int threads;
+ private final String base;
+
+ public LuceneIndexModule() {
+ this(null, 0, null);
+ }
+
+ public LuceneIndexModule(Integer singleVersion, int threads,
+ String base) {
+ this.singleVersion = singleVersion;
+ this.threads = threads;
+ this.base = base;
+ }
+
+ @Override
+ protected void configure() {
+ install(new FactoryModule() {
+ @Override
+ public void configure() {
+ factory(LuceneChangeIndex.Factory.class);
+ }
+ });
+ install(new IndexModule(threads));
+ if (singleVersion == null && base == null) {
+ install(new MultiVersionModule());
+ } else {
+ install(new SingleVersionModule());
+ }
+ }
+
+ private class MultiVersionModule extends LifecycleModule {
+ @Override
+ public void configure() {
+ install(new FactoryModule() {
+ @Override
+ public void configure() {
+ factory(OnlineReindexer.Factory.class);
+ }
+ });
+ listener().to(LuceneVersionManager.class);
+ }
+ }
+
+ private class SingleVersionModule extends LifecycleModule {
+ @Override
+ public void configure() {
+ listener().to(SingleVersionListener.class);
+ }
+
+ @Provides
+ @Singleton
+ LuceneChangeIndex getIndex(LuceneChangeIndex.Factory factory,
+ SitePaths sitePaths) {
+ Schema<ChangeData> schema = singleVersion != null
+ ? ChangeSchemas.get(singleVersion)
+ : ChangeSchemas.getLatest();
+ return factory.create(schema, base);
+ }
+ }
+
+ @Singleton
+ static class SingleVersionListener implements LifecycleListener {
+ private final IndexCollection indexes;
+ private final LuceneChangeIndex index;
+
+ @Inject
+ SingleVersionListener(IndexCollection indexes,
+ LuceneChangeIndex index) {
+ this.indexes = indexes;
+ this.index = index;
+ }
+
+ @Override
+ public void start() {
+ indexes.setSearchIndex(index);
+ indexes.addWriteIndex(index);
+ }
+
+ @Override
+ public void stop() {
+ index.close();
+ }
+ }
+}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
new file mode 100644
index 0000000..f809f49
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -0,0 +1,224 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.lucene;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.TreeMap;
+
+@Singleton
+class LuceneVersionManager implements LifecycleListener {
+ private static final Logger log = LoggerFactory
+ .getLogger(LuceneVersionManager.class);
+
+ private static final String CHANGES_PREFIX = "changes_";
+
+ private static class Version {
+ private final Schema<ChangeData> schema;
+ private final int version;
+ private final boolean exists;
+ private final boolean ready;
+
+ private Version(Schema<ChangeData> schema, int version, boolean exists,
+ boolean ready) {
+ checkArgument(schema == null || schema.getVersion() == version);
+ this.schema = schema;
+ this.version = version;
+ this.exists = exists;
+ this.ready = ready;
+ }
+ }
+
+ static File getDir(SitePaths sitePaths, Schema<ChangeData> schema) {
+ return new File(sitePaths.index_dir, String.format("%s%04d",
+ CHANGES_PREFIX, schema.getVersion()));
+ }
+
+ static FileBasedConfig loadGerritIndexConfig(SitePaths sitePaths)
+ throws ConfigInvalidException, IOException {
+ FileBasedConfig cfg = new FileBasedConfig(
+ new File(sitePaths.index_dir, "gerrit_index.config"), FS.detect());
+ cfg.load();
+ return cfg;
+ }
+
+ static void setReady(Config cfg, int version, boolean ready) {
+ cfg.setBoolean("index", Integer.toString(version), "ready", ready);
+ }
+
+ private static boolean getReady(Config cfg, int version) {
+ return cfg.getBoolean("index", Integer.toString(version), "ready", false);
+ }
+
+ private final SitePaths sitePaths;
+ private final LuceneChangeIndex.Factory indexFactory;
+ private final IndexCollection indexes;
+ private final OnlineReindexer.Factory reindexerFactory;
+
+ @Inject
+ LuceneVersionManager(
+ SitePaths sitePaths,
+ LuceneChangeIndex.Factory indexFactory,
+ IndexCollection indexes,
+ OnlineReindexer.Factory reindexerFactory) {
+ this.sitePaths = sitePaths;
+ this.indexFactory = indexFactory;
+ this.indexes = indexes;
+ this.reindexerFactory = reindexerFactory;
+ }
+
+ @Override
+ public void start() {
+ FileBasedConfig cfg;
+ try {
+ cfg = loadGerritIndexConfig(sitePaths);
+ } catch (ConfigInvalidException e) {
+ throw fail(e);
+ } catch (IOException e) {
+ throw fail(e);
+ }
+
+ if (!sitePaths.index_dir.exists()) {
+ throw new ProvisionException("No index versions ready; run Reindex");
+ } else if (!sitePaths.index_dir.isDirectory()) {
+ log.warn("Not a directory: %s", sitePaths.index_dir.getAbsolutePath());
+ throw new ProvisionException("No index versions ready; run Reindex");
+ }
+
+ TreeMap<Integer, Version> versions = scanVersions(cfg);
+ // Search from the most recent ready version.
+ // Write to the most recent ready version and the most recent version.
+ Version search = null;
+ List<Version> write = Lists.newArrayListWithCapacity(2);
+ for (Version v : versions.descendingMap().values()) {
+ if (v.schema == null) {
+ continue;
+ }
+ if (write.isEmpty()) {
+ write.add(v);
+ }
+ if (v.ready) {
+ search = v;
+ if (!write.contains(v)) {
+ write.add(v);
+ }
+ break;
+ }
+ }
+ if (search == null) {
+ throw new ProvisionException("No index versions ready; run Reindex");
+ }
+
+ markNotReady(cfg, versions.values(), write);
+ LuceneChangeIndex searchIndex = indexFactory.create(search.schema, null);
+ indexes.setSearchIndex(searchIndex);
+ for (Version v : write) {
+ if (v.schema != null) {
+ if (v.version != search.version) {
+ indexes.addWriteIndex(indexFactory.create(v.schema, null));
+ } else {
+ indexes.addWriteIndex(searchIndex);
+ }
+ }
+ }
+
+ int latest = write.get(0).version;
+ if (latest != search.version) {
+ reindexerFactory.create(latest).start();
+ }
+ }
+
+ private TreeMap<Integer, Version> scanVersions(Config cfg) {
+ TreeMap<Integer, Version> versions = Maps.newTreeMap();
+ for (Schema<ChangeData> schema : ChangeSchemas.ALL.values()) {
+ File f = getDir(sitePaths, schema);
+ boolean exists = f.exists() && f.isDirectory();
+ if (f.exists() && !f.isDirectory()) {
+ log.warn("Not a directory: %s", f.getAbsolutePath());
+ }
+ int v = schema.getVersion();
+ versions.put(v, new Version(schema, v, exists, getReady(cfg, v)));
+ }
+
+ for (File f : sitePaths.index_dir.listFiles()) {
+ if (!f.getName().startsWith(CHANGES_PREFIX)) {
+ continue;
+ }
+ String versionStr = f.getName().substring(CHANGES_PREFIX.length());
+ Integer v = Ints.tryParse(versionStr);
+ if (v == null || versionStr.length() != 4) {
+ log.warn("Unrecognized version in index directory: {}",
+ f.getAbsolutePath());
+ continue;
+ }
+ if (!versions.containsKey(v)) {
+ versions.put(v, new Version(null, v, true, getReady(cfg, v)));
+ }
+ }
+ return versions;
+ }
+
+ private void markNotReady(FileBasedConfig cfg, Iterable<Version> versions,
+ Collection<Version> inUse) {
+ boolean dirty = false;
+ for (Version v : versions) {
+ if (!inUse.contains(v) && v.exists) {
+ setReady(cfg, v.version, false);
+ dirty = true;
+ }
+ }
+ if (dirty) {
+ try {
+ cfg.save();
+ } catch (IOException e) {
+ throw fail(e);
+ }
+ }
+ }
+
+ private ProvisionException fail(Throwable t) {
+ ProvisionException e = new ProvisionException("Error scanning indexes");
+ e.initCause(t);
+ throw e;
+ }
+
+ @Override
+ public void stop() {
+ }
+}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
new file mode 100644
index 0000000..08338eb
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/OnlineReindexer.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.lucene;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.server.index.ChangeBatchIndexer;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+
+public class OnlineReindexer {
+ private static final Logger log = LoggerFactory
+ .getLogger(OnlineReindexer.class);
+
+ public interface Factory {
+ OnlineReindexer create(int version);
+ }
+
+ private final IndexCollection indexes;
+ private final ChangeBatchIndexer batchIndexer;
+ private final ProjectCache projectCache;
+ private final int version;
+
+ @Inject
+ OnlineReindexer(
+ IndexCollection indexes,
+ ChangeBatchIndexer batchIndexer,
+ ProjectCache projectCache,
+ @Assisted int version) {
+ this.indexes = indexes;
+ this.batchIndexer = batchIndexer;
+ this.projectCache = projectCache;
+ this.version = version;
+ }
+
+ public void start() {
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ reindex();
+ }
+ };
+ t.setName(String.format("Reindex v%d-v%d",
+ version(indexes.getSearchIndex()), version));
+ t.start();
+ }
+
+ private static int version(ChangeIndex i) {
+ return i.getSchema().getVersion();
+ }
+
+ private void reindex() {
+ ChangeIndex index = checkNotNull(indexes.getWriteIndex(version),
+ "not an active write schema version: %s", version);
+ log.info("Starting online reindex from schema version {} to {}",
+ version(indexes.getSearchIndex()), version(index));
+ ChangeBatchIndexer.Result result = batchIndexer.indexAll(
+ index, projectCache.all(), -1, -1, null, null);
+ if (!result.success()) {
+ log.error("Online reindex of schema version {} failed", version(index));
+ return;
+ }
+
+ indexes.setSearchIndex(index);
+ log.info("Reindex complete, using schema version {}", version(index));
+ try {
+ index.markReady(true);
+ } catch (IOException e) {
+ log.warn("Error activating new schema version {}", version(index));
+ }
+
+ List<ChangeIndex> toRemove = Lists.newArrayListWithExpectedSize(1);
+ for (ChangeIndex i : indexes.getWriteIndexes()) {
+ if (version(i) != version(index)) {
+ toRemove.add(i);
+ }
+ }
+ for (ChangeIndex i : toRemove) {
+ try {
+ i.markReady(false);
+ indexes.removeWriteIndex(version(i));
+ } catch (IOException e) {
+ log.warn("Error deactivating old schema version {}", version(i));
+ }
+ }
+ }
+}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
new file mode 100644
index 0000000..f491bc2
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -0,0 +1,232 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.lucene;
+
+import static org.apache.lucene.search.BooleanClause.Occur.MUST;
+import static org.apache.lucene.search.BooleanClause.Occur.MUST_NOT;
+import static org.apache.lucene.search.BooleanClause.Occur.SHOULD;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.FieldType;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.RegexPredicate;
+import com.google.gerrit.server.index.TimestampRangePredicate;
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.NotPredicate;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.SortKeyPredicate;
+
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.FuzzyQuery;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.NumericRangeQuery;
+import org.apache.lucene.search.PrefixQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.RegexpQuery;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.NumericUtils;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+public class QueryBuilder {
+ private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
+
+ public static Term idTerm(ChangeData cd) {
+ return intTerm(ID_FIELD, cd.getId().get());
+ }
+
+ public static Query toQuery(Predicate<ChangeData> p)
+ throws QueryParseException {
+ if (p instanceof AndPredicate) {
+ return and(p);
+ } else if (p instanceof OrPredicate) {
+ return or(p);
+ } else if (p instanceof NotPredicate) {
+ return not(p);
+ } else if (p instanceof IndexPredicate) {
+ return fieldQuery((IndexPredicate<ChangeData>) p);
+ } else {
+ throw new QueryParseException("cannot create query for index: " + p);
+ }
+ }
+
+ private static Query or(Predicate<ChangeData> p) throws QueryParseException {
+ try {
+ BooleanQuery q = new BooleanQuery();
+ for (int i = 0; i < p.getChildCount(); i++) {
+ q.add(toQuery(p.getChild(i)), SHOULD);
+ }
+ return q;
+ } catch (BooleanQuery.TooManyClauses e) {
+ throw new QueryParseException("cannot create query for index: " + p, e);
+ }
+ }
+
+ private static Query and(Predicate<ChangeData> p) throws QueryParseException {
+ try {
+ BooleanQuery b = new BooleanQuery();
+ List<Query> not = Lists.newArrayListWithCapacity(p.getChildCount());
+ for (int i = 0; i < p.getChildCount(); i++) {
+ Predicate<ChangeData> c = p.getChild(i);
+ if (c instanceof NotPredicate) {
+ Predicate<ChangeData> n = c.getChild(0);
+ if (n instanceof TimestampRangePredicate) {
+ b.add(notTimestamp((TimestampRangePredicate<ChangeData>) n), MUST);
+ } else {
+ not.add(toQuery(n));
+ }
+ } else {
+ b.add(toQuery(c), MUST);
+ }
+ }
+ for (Query q : not) {
+ b.add(q, MUST_NOT);
+ }
+ return b;
+ } catch (BooleanQuery.TooManyClauses e) {
+ throw new QueryParseException("cannot create query for index: " + p, e);
+ }
+ }
+
+ private static Query not(Predicate<ChangeData> p) throws QueryParseException {
+ Predicate<ChangeData> n = p.getChild(0);
+ if (n instanceof TimestampRangePredicate) {
+ return notTimestamp((TimestampRangePredicate<ChangeData>) n);
+ }
+
+ // Lucene does not support negation, start with all and subtract.
+ BooleanQuery q = new BooleanQuery();
+ q.add(new MatchAllDocsQuery(), MUST);
+ q.add(toQuery(n), MUST_NOT);
+ return q;
+ }
+
+ private static Query fieldQuery(IndexPredicate<ChangeData> p)
+ throws QueryParseException {
+ if (p.getType() == FieldType.INTEGER) {
+ return intQuery(p);
+ } else if (p.getType() == FieldType.TIMESTAMP) {
+ return timestampQuery(p);
+ } else if (p.getType() == FieldType.EXACT) {
+ return exactQuery(p);
+ } else if (p.getType() == FieldType.PREFIX) {
+ return prefixQuery(p);
+ } else if (p.getType() == FieldType.FULL_TEXT) {
+ return fullTextQuery(p);
+ } else if (p instanceof SortKeyPredicate) {
+ return sortKeyQuery((SortKeyPredicate) p);
+ } else {
+ throw badFieldType(p.getType());
+ }
+ }
+
+ private static Term intTerm(String name, int value) {
+ BytesRef bytes = new BytesRef(NumericUtils.BUF_SIZE_INT);
+ NumericUtils.intToPrefixCodedBytes(value, 0, bytes);
+ return new Term(name, bytes);
+ }
+
+ private static Query intQuery(IndexPredicate<ChangeData> p)
+ throws QueryParseException {
+ int value;
+ try {
+ // Can't use IntPredicate because it and IndexPredicate are different
+ // subclasses of OperatorPredicate.
+ value = Integer.valueOf(p.getValue());
+ } catch (IllegalArgumentException e) {
+ throw new QueryParseException("not an integer: " + p.getValue());
+ }
+ return new TermQuery(intTerm(p.getField().getName(), value));
+ }
+
+ private static Query sortKeyQuery(SortKeyPredicate p) {
+ return NumericRangeQuery.newLongRange(
+ p.getField().getName(),
+ p.getMinValue() != Long.MIN_VALUE ? p.getMinValue() : null,
+ p.getMaxValue() != Long.MAX_VALUE ? p.getMaxValue() : null,
+ true, true);
+ }
+
+ private static Query timestampQuery(IndexPredicate<ChangeData> p)
+ throws QueryParseException {
+ if (p instanceof TimestampRangePredicate) {
+ TimestampRangePredicate<ChangeData> r =
+ (TimestampRangePredicate<ChangeData>) p;
+ return NumericRangeQuery.newIntRange(
+ r.getField().getName(),
+ toIndexTime(r.getMinTimestamp()),
+ toIndexTime(r.getMaxTimestamp()),
+ true, true);
+ }
+ throw new QueryParseException("not a timestamp: " + p);
+ }
+
+ private static Query notTimestamp(TimestampRangePredicate<ChangeData> r)
+ throws QueryParseException {
+ if (r.getMinTimestamp().getTime() == 0) {
+ return NumericRangeQuery.newIntRange(
+ r.getField().getName(),
+ toIndexTime(r.getMaxTimestamp()),
+ null,
+ true, true);
+ }
+ throw new QueryParseException("cannot negate: " + r);
+ }
+
+ private static Query exactQuery(IndexPredicate<ChangeData> p) {
+ if (p instanceof RegexPredicate<?>) {
+ return regexQuery(p);
+ } else {
+ return new TermQuery(new Term(p.getField().getName(), p.getValue()));
+ }
+ }
+
+ private static Query regexQuery(IndexPredicate<ChangeData> p) {
+ String re = p.getValue();
+ if (re.startsWith("^")) {
+ re = re.substring(1);
+ }
+ if (re.endsWith("$") && !re.endsWith("\\$")) {
+ re = re.substring(0, re.length() - 1);
+ }
+ return new RegexpQuery(new Term(p.getField().getName(), re));
+ }
+
+ private static Query prefixQuery(IndexPredicate<ChangeData> p) {
+ return new PrefixQuery(new Term(p.getField().getName(), p.getValue()));
+ }
+
+ private static Query fullTextQuery(IndexPredicate<ChangeData> p) {
+ return new FuzzyQuery(new Term(p.getField().getName(), p.getValue()));
+ }
+
+ public static int toIndexTime(Timestamp ts) {
+ return (int) (ts.getTime() / 60000);
+ }
+
+ public static IllegalArgumentException badFieldType(FieldType<?> t) {
+ return new IllegalArgumentException("unknown index field type " + t);
+ }
+
+ private QueryBuilder() {
+ }
+}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
new file mode 100644
index 0000000..60bc547
--- /dev/null
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
@@ -0,0 +1,196 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.lucene;
+
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.AbstractFuture;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.NRTManager;
+import org.apache.lucene.search.NRTManager.TrackingIndexWriter;
+import org.apache.lucene.search.NRTManagerReopenThread;
+import org.apache.lucene.search.ReferenceManager.RefreshListener;
+import org.apache.lucene.search.SearcherFactory;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Piece of the change index that is implemented as a separate Lucene index. */
+class SubIndex {
+ private static final Logger log = LoggerFactory.getLogger(SubIndex.class);
+
+ private final Directory dir;
+ private final TrackingIndexWriter writer;
+ private final NRTManager nrtManager;
+ private final NRTManagerReopenThread reopenThread;
+ private final ConcurrentMap<RefreshListener, Boolean> refreshListeners;
+
+ SubIndex(File file, IndexWriterConfig writerConfig) throws IOException {
+ dir = FSDirectory.open(file);
+ writer = new NRTManager.TrackingIndexWriter(new IndexWriter(dir, writerConfig));
+ nrtManager = new NRTManager(writer, new SearcherFactory());
+
+ refreshListeners = Maps.newConcurrentMap();
+ nrtManager.addListener(new RefreshListener() {
+ @Override
+ public void beforeRefresh() throws IOException {
+ }
+
+ @Override
+ public void afterRefresh(boolean didRefresh) throws IOException {
+ for (RefreshListener l : refreshListeners.keySet()) {
+ l.afterRefresh(didRefresh);
+ }
+ }
+ });
+
+ reopenThread = new NRTManagerReopenThread(
+ nrtManager,
+ 0.500 /* maximum stale age (seconds) */,
+ 0.010 /* minimum stale age (seconds) */);
+ reopenThread.setName("NRT " + file.getName());
+ reopenThread.setPriority(Math.min(
+ Thread.currentThread().getPriority() + 2,
+ Thread.MAX_PRIORITY));
+ reopenThread.setDaemon(true);
+ reopenThread.start();
+ }
+
+ void close() {
+ reopenThread.close();
+ try {
+ nrtManager.close();
+ } catch (IOException e) {
+ log.warn("error closing Lucene searcher", e);
+ }
+ try {
+ writer.getIndexWriter().close();
+ } catch (IOException e) {
+ log.warn("error closing Lucene writer", e);
+ }
+ try {
+ dir.close();
+ } catch (IOException e) {
+ log.warn("error closing Lucene directory", e);
+ }
+ }
+
+ ListenableFuture<Void> insert(Document doc) throws IOException {
+ return new NrtFuture(writer.addDocument(doc));
+ }
+
+ ListenableFuture<Void> replace(Term term, Document doc) throws IOException {
+ return new NrtFuture(writer.updateDocument(term, doc));
+ }
+
+ ListenableFuture<Void> delete(Term term) throws IOException {
+ return new NrtFuture(writer.deleteDocuments(term));
+ }
+
+ void deleteAll() throws IOException {
+ writer.deleteAll();
+ }
+
+ IndexSearcher acquire() throws IOException {
+ return nrtManager.acquire();
+ }
+
+ void release(IndexSearcher searcher) throws IOException {
+ nrtManager.release(searcher);
+ }
+
+ private final class NrtFuture extends AbstractFuture<Void>
+ implements RefreshListener {
+ private final long gen;
+ private final AtomicBoolean hasListeners = new AtomicBoolean();
+
+ NrtFuture(long gen) {
+ this.gen = gen;
+ }
+
+ @Override
+ public Void get() throws InterruptedException, ExecutionException {
+ if (!isDone()) {
+ nrtManager.waitForGeneration(gen);
+ set(null);
+ }
+ return super.get();
+ }
+
+ @Override
+ public Void get(long timeout, TimeUnit unit) throws InterruptedException,
+ TimeoutException, ExecutionException {
+ if (!isDone()) {
+ nrtManager.waitForGeneration(gen, timeout, unit);
+ set(null);
+ }
+ return super.get(timeout, unit);
+ }
+
+ @Override
+ public boolean isDone() {
+ if (super.isDone()) {
+ return true;
+ } else if (gen <= nrtManager.getCurrentSearchingGen()) {
+ set(null);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void addListener(Runnable listener, Executor executor) {
+ if (hasListeners.compareAndSet(false, true) && !isDone()) {
+ nrtManager.addListener(this);
+ }
+ super.addListener(listener, executor);
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ if (hasListeners.get()) {
+ refreshListeners.put(this, true);
+ }
+ return super.cancel(mayInterruptIfRunning);
+ }
+
+ @Override
+ public void beforeRefresh() throws IOException {
+ }
+
+ @Override
+ public void afterRefresh(boolean didRefresh) throws IOException {
+ if (gen <= nrtManager.getCurrentSearchingGen()) {
+ refreshListeners.remove(this);
+ set(null);
+ }
+ }
+ }
+}
diff --git a/gerrit-main/BUCK b/gerrit-main/BUCK
new file mode 100644
index 0000000..da39eec
--- /dev/null
+++ b/gerrit-main/BUCK
@@ -0,0 +1,13 @@
+java_binary(
+ name = 'main_bin',
+ main_class = 'Main',
+ deps = [':main_lib'],
+ visibility = ['PUBLIC'],
+)
+
+java_library(
+ name = 'main_lib',
+ srcs = ['src/main/java/Main.java'],
+ deps = ['//gerrit-launcher:launcher'],
+ visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
deleted file mode 100644
index 46b0a9f..0000000
--- a/gerrit-main/pom.xml
+++ /dev/null
@@ -1,72 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-main</artifactId>
- <name>Gerrit Code Review - Main</name>
-
- <description>
- Main class to bootstrap out of a WAR
- </description>
-
- <dependencies>
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-launcher</artifactId>
- <version>${project.version}</version>
- </dependency>
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <configuration>
- <source>1.2</source>
- <target>1.2</target>
- <encoding>UTF-8</encoding>
- </configuration>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-shade-plugin</artifactId>
- <executions>
- <execution>
- <phase>package</phase>
- <goals>
- <goal>shade</goal>
- </goals>
- <configuration>
- <createDependencyReducedPom>false</createDependencyReducedPom>
- </configuration>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/gerrit-openid/BUCK b/gerrit-openid/BUCK
new file mode 100644
index 0000000..0801534
--- /dev/null
+++ b/gerrit-openid/BUCK
@@ -0,0 +1,23 @@
+java_library2(
+ name = 'openid',
+ srcs = glob(['src/main/java/**/*.java']),
+ resources = glob(['src/main/resources/**/*']),
+ deps = [
+ '//gerrit-common:server',
+ '//gerrit-extension-api:api',
+ '//gerrit-gwtexpui:server',
+ '//gerrit-httpd:httpd',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//lib:guava',
+ '//lib:gwtorm',
+ '//lib:jsr305',
+ '//lib/guice:guice',
+ '//lib/guice:guice-servlet',
+ '//lib/jgit:jgit',
+ '//lib/log:api',
+ '//lib/openid:consumer',
+ ],
+ compile_deps = ['//lib:servlet-api-3_0'],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-openid/pom.xml b/gerrit-openid/pom.xml
deleted file mode 100644
index 1e73867..0000000
--- a/gerrit-openid/pom.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-openid</artifactId>
- <name>Gerrit Code Review - OpenID servlet and RPC</name>
-
- <description>
- OpenID
- </description>
-
- <dependencies>
- <dependency>
- <groupId>org.apache.tomcat</groupId>
- <artifactId>servlet-api</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.code.findbugs</groupId>
- <artifactId>jsr305</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit.http.server</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.openid4java</groupId>
- <artifactId>openid4java</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-httpd</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-server</artifactId>
- <version>${project.version}</version>
- </dependency>
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-source-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>jar</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index 678ec00..796bdb4 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -79,7 +79,7 @@
"openid", "maxRedirectUrlLength",
10);
- if (Strings.isNullOrEmpty(urlProvider.get())) {
+ if (urlProvider == null || Strings.isNullOrEmpty(urlProvider.get())) {
log.error("gerrit.canonicalWebUrl must be set in gerrit.config");
}
@@ -229,7 +229,7 @@
private void sendForm(HttpServletRequest req, HttpServletResponse res,
boolean link, @Nullable String errorMessage) throws IOException {
String self = req.getRequestURI();
- String cancel = Objects.firstNonNull(urlProvider.get(), "/");
+ String cancel = Objects.firstNonNull(urlProvider != null ? urlProvider.get() : "/", "/");
String token = getToken(req);
if (!token.equals("/")) {
cancel += "#" + token;
diff --git a/gerrit-patch-commonsnet/BUCK b/gerrit-patch-commonsnet/BUCK
new file mode 100644
index 0000000..53b382f
--- /dev/null
+++ b/gerrit-patch-commonsnet/BUCK
@@ -0,0 +1,11 @@
+java_library(
+ name = 'commons-net',
+ srcs = glob(['src/main/java/org/apache/commons/net/**/*.java']),
+ deps = [
+ '//gerrit-util-ssl:ssl',
+ '//lib/commons:codec',
+ '//lib/commons:net',
+ '//lib/log:api',
+ ],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
deleted file mode 100644
index 82c1d82..0000000
--- a/gerrit-patch-commonsnet/pom.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-patch-commonsnet</artifactId>
- <name>Gerrit Code Review - Patch commons-net</name>
-
- <description>
- Hacks to expose package-private data from commons-net to Gerrit
- </description>
-
- <dependencies>
- <dependency>
- <groupId>commons-net</groupId>
- <artifactId>commons-net</artifactId>
- </dependency>
-
- <dependency>
- <groupId>commons-codec</groupId>
- <artifactId>commons-codec</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-util-ssl</artifactId>
- <version>${project.version}</version>
- </dependency>
- </dependencies>
-</project>
diff --git a/gerrit-patch-jgit/BUCK b/gerrit-patch-jgit/BUCK
new file mode 100644
index 0000000..18890ac
--- /dev/null
+++ b/gerrit-patch-jgit/BUCK
@@ -0,0 +1,32 @@
+SRC = 'src/main/java/org/eclipse/jgit/'
+
+gwt_module(
+ name = 'client',
+ srcs = [
+ SRC + 'diff/Edit_JsonSerializer.java',
+ SRC + 'diff/ReplaceEdit.java',
+ ],
+ gwtxml = SRC + 'JGit.gwt.xml',
+ deps = [
+ '//lib:gwtjsonrpc',
+ '//lib/gwt:user',
+ '//lib/jgit:jgit',
+ '//lib/jgit:Edit',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+java_library(
+ name = 'server',
+ srcs = [
+ SRC + 'diff/EditDeserializer.java',
+ SRC + 'diff/ReplaceEdit.java',
+ SRC + 'internal/storage/file/WindowCacheStatAccessor.java',
+ SRC + 'lib/ObjectIdSerialization.java',
+ ],
+ deps = [
+ '//lib:gson',
+ '//lib/jgit:jgit',
+ ],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
deleted file mode 100644
index a1b85b3..0000000
--- a/gerrit-patch-jgit/pom.xml
+++ /dev/null
@@ -1,68 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-patch-jgit</artifactId>
- <name>Gerrit Code Review - Patch JGit</name>
-
- <description>
- Hacks to expose package-private data from JGit to Gerrit
- </description>
-
- <dependencies>
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit</artifactId>
- </dependency>
-
- <dependency>
- <groupId>gwtjsonrpc</groupId>
- <artifactId>gwtjsonrpc</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.gwt</groupId>
- <artifactId>gwt-user</artifactId>
- <scope>provided</scope>
- </dependency>
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-source-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>jar</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
new file mode 100644
index 0000000..2a88694
--- /dev/null
+++ b/gerrit-pgm/BUCK
@@ -0,0 +1,92 @@
+INIT_SRCS = ['src/main/java/com/google/gerrit/pgm/' + n for n in [
+ 'init/InitFlags.java',
+ 'init/InitStep.java',
+ 'init/InitStep.java',
+ 'init/InstallPlugins.java',
+ 'init/Section.java',
+ 'util/ConsoleUI.java',
+ 'util/Die.java',
+]]
+
+java_library(
+ name = 'init-api',
+ srcs = INIT_SRCS,
+ deps = [
+ '//gerrit-server:server',
+ '//lib:jsr305',
+ '//lib/guice:guice',
+ '//lib/guice:guice-assistedinject',
+ '//lib/jgit:jgit',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+java_sources(
+ name = 'init-api-src',
+ srcs = INIT_SRCS,
+ visibility = ['PUBLIC'],
+)
+
+java_library2(
+ name = 'pgm',
+ srcs = glob(['src/main/java/**/*.java'], excludes = INIT_SRCS),
+ resources = glob(['src/main/resources/**/*']),
+ deps = [
+ ':init-api',
+ '//gerrit-cache-h2:cache-h2',
+ '//gerrit-common:server',
+ '//gerrit-extension-api:api',
+ '//gerrit-gwtexpui:server',
+ '//gerrit-httpd:httpd',
+ '//gerrit-lucene:lucene',
+ '//gerrit-openid:openid',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//gerrit-server/src/main/prolog:common',
+ '//gerrit-solr:solr',
+ '//gerrit-sshd:sshd',
+ '//gerrit-util-cli:cli',
+ '//lib:args4j',
+ '//lib:guava',
+ '//lib:gwtorm',
+ '//lib:gwtjsonrpc',
+ '//lib:h2',
+ '//lib:jsr305',
+ '//lib:servlet-api-3_0',
+ '//lib/commons:dbcp',
+ '//lib/guice:guice',
+ '//lib/guice:guice-assistedinject',
+ '//lib/guice:guice-servlet',
+ '//lib/jetty:server',
+ '//lib/jetty:servlet',
+ '//lib/jgit:jgit',
+ '//lib/log:api',
+ '//lib/log:log4j',
+ '//lib/lucene:core',
+ '//lib/mina:sshd',
+ '//lib/prolog:prolog-cafe',
+ ],
+ compile_deps = ['//gerrit-launcher:launcher'],
+ visibility = [
+ '//:',
+ '//gerrit-acceptance-tests/...',
+ '//tools/eclipse:classpath',
+ '//Documentation:licenses.txt',
+ ],
+)
+
+java_test(
+ name = 'pgm_tests',
+ srcs = glob(['src/test/java/**/*.java']),
+ deps = [
+ ':init-api',
+ ':pgm',
+ '//gerrit-server:server',
+ '//lib:junit',
+ '//lib:easymock',
+ '//lib/guice:guice',
+ '//lib/jgit:jgit',
+ '//lib/jgit:junit',
+ ],
+ source_under_test = [':pgm'],
+)
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
deleted file mode 100644
index 9acb912..0000000
--- a/gerrit-pgm/pom.xml
+++ /dev/null
@@ -1,109 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-pgm</artifactId>
- <name>Gerrit Code Review - Pgm</name>
-
- <description>
- Command line executables
- </description>
-
- <dependencies>
- <dependency>
- <groupId>log4j</groupId>
- <artifactId>log4j</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- </dependency>
-
- <dependency>
- <groupId>postgresql</groupId>
- <artifactId>postgresql</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-main</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-util-cli</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-server</artifactId>
- <version>${project.version}</version>
- <exclusions>
- <exclusion>
- <groupId>org.apache.tomcat</groupId>
- <artifactId>servlet-api</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-openid</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-sshd</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-httpd</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-servlet</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.apache.tomcat</groupId>
- <artifactId>tomcat-servlet-api</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit.junit</artifactId>
- </dependency>
- </dependencies>
-</project>
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index ca98a84..891dadc 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -28,6 +28,7 @@
import com.google.gerrit.httpd.auth.openid.OpenIdModule;
import com.google.gerrit.httpd.plugins.HttpPluginModule;
import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.lucene.LuceneIndexModule;
import com.google.gerrit.pgm.http.jetty.GetUserFilter;
import com.google.gerrit.pgm.http.jetty.JettyEnv;
import com.google.gerrit.pgm.http.jetty.JettyModule;
@@ -38,44 +39,37 @@
import com.google.gerrit.pgm.util.RuntimeShutdown;
import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.reviewdb.client.AuthType;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.InternalAccountDirectory;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.AuthConfigModule;
import com.google.gerrit.server.config.CanonicalWebUrlModule;
import com.google.gerrit.server.config.CanonicalWebUrlProvider;
import com.google.gerrit.server.config.GerritGlobalModule;
-import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.MasterNodeStartup;
import com.google.gerrit.server.contact.HttpContactStoreConnection;
import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.NoIndexModule;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
import com.google.gerrit.server.mail.SmtpEmailSender;
import com.google.gerrit.server.patch.IntraLineWorkerPool;
import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
-import com.google.gerrit.server.plugins.PluginModule;
-import com.google.gerrit.server.schema.SchemaUpdater;
+import com.google.gerrit.server.plugins.PluginRestApiModule;
import com.google.gerrit.server.schema.SchemaVersionCheck;
-import com.google.gerrit.server.schema.UpdateUI;
import com.google.gerrit.server.ssh.NoSshKeyCache;
import com.google.gerrit.server.ssh.NoSshModule;
+import com.google.gerrit.solr.SolrIndexModule;
import com.google.gerrit.sshd.SshKeyCacheImpl;
import com.google.gerrit.sshd.SshModule;
import com.google.gerrit.sshd.commands.MasterCommandModule;
import com.google.gerrit.sshd.commands.SlaveCommandModule;
-import com.google.gwtorm.jdbc.JdbcExecutor;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.gwtorm.server.StatementExecutor;
import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.Provider;
-import org.eclipse.jgit.lib.Config;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -121,6 +115,10 @@
@Option(name = "--headless", usage = "Don't start the UI frontend")
private boolean headless;
+ @Option(name = "--init", aliases = {"-i"},
+ usage = "Init site before starting the daemon")
+ private boolean doInit;
+
private final LifecycleManager manager = new LifecycleManager();
private Injector dbInjector;
private Injector cfgInjector;
@@ -141,6 +139,13 @@
@Override
public int run() throws Exception {
+ if (doInit) {
+ try {
+ new Init(getSitePath()).run();
+ } catch (Exception e) {
+ throw die("Init failed", e);
+ }
+ }
mustHaveValidSite();
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
@@ -176,7 +181,6 @@
sysInjector = createSysInjector();
sysInjector.getInstance(PluginGuiceEnvironment.class)
.setCfgInjector(cfgInjector);
- sysInjector.getInstance(SchemaUpgrade.class).upgradeSchema();
manager.add(dbInjector, cfgInjector, sysInjector);
if (sshd) {
@@ -228,74 +232,6 @@
}
}
- static class SchemaUpgrade {
-
- private final Config config;
- private final SchemaUpdater updater;
- private final SchemaFactory<ReviewDb> schema;
-
- @Inject
- SchemaUpgrade(@GerritServerConfig Config config, SchemaUpdater updater,
- SchemaFactory<ReviewDb> schema) {
- this.config = config;
- this.updater = updater;
- this.schema = schema;
- }
-
- void upgradeSchema() throws OrmException {
- SchemaUpgradePolicy policy =
- config.getEnum("site", null, "upgradeSchemaOnStartup",
- SchemaUpgradePolicy.OFF);
- if (policy == SchemaUpgradePolicy.AUTO
- || policy == SchemaUpgradePolicy.AUTO_NO_PRUNE) {
- final List<String> pruneList = new ArrayList<String>();
- updater.update(new UpdateUI() {
- @Override
- public void message(String msg) {
- log.info(msg);
- }
-
- @Override
- public boolean yesno(boolean def, String msg) {
- return true;
- }
-
- @Override
- public boolean isBatch() {
- return true;
- }
-
- @Override
- public void pruneSchema(StatementExecutor e, List<String> prune) {
- for (String p : prune) {
- if (!pruneList.contains(p)) {
- pruneList.add(p);
- }
- }
- }
- });
-
- if (!pruneList.isEmpty() && policy == SchemaUpgradePolicy.AUTO) {
- log.info("Pruning: " + pruneList.toString());
- final JdbcSchema db = (JdbcSchema) schema.open();
- try {
- final JdbcExecutor e = new JdbcExecutor(db);
- try {
- for (String sql : pruneList) {
- e.execute(sql);
- }
- } finally {
- e.close();
- }
- } finally {
- db.close();
- }
- }
- }
- }
- }
-
-
private String myVersion() {
return com.google.gerrit.common.Version.getVersion();
}
@@ -315,10 +251,23 @@
modules.add(new ReceiveCommitsExecutorModule());
modules.add(new IntraLineWorkerPool.Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+ modules.add(new InternalAccountDirectory.Module());
modules.add(new DefaultCacheFactory.Module());
modules.add(new SmtpEmailSender.Module());
modules.add(new SignedTokenEmailTokenVerifier.Module());
- modules.add(new PluginModule());
+ modules.add(new PluginRestApiModule());
+ AbstractModule changeIndexModule;
+ switch (IndexModule.getIndexType(cfgInjector)) {
+ case LUCENE:
+ changeIndexModule = new LuceneIndexModule();
+ break;
+ case SOLR:
+ changeIndexModule = new SolrIndexModule();
+ break;
+ default:
+ changeIndexModule = new NoIndexModule();
+ }
+ modules.add(changeIndexModule);
if (httpd) {
modules.add(new CanonicalWebUrlModule() {
@Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
index 3c7822c..ae1ab71 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -17,10 +17,17 @@
import static com.google.gerrit.server.schema.DataSourceProvider.Context.SINGLE_USER;
import static com.google.inject.Stage.PRODUCTION;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.pgm.init.Browser;
import com.google.gerrit.pgm.init.InitFlags;
import com.google.gerrit.pgm.init.InitModule;
+import com.google.gerrit.pgm.init.InitPlugins;
+import com.google.gerrit.pgm.init.InitPlugins.PluginData;
+import com.google.gerrit.pgm.init.InstallPlugins;
import com.google.gerrit.pgm.init.SitePathInitializer;
import com.google.gerrit.pgm.util.ConsoleUI;
import com.google.gerrit.pgm.util.Die;
@@ -45,6 +52,7 @@
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
import com.google.inject.spi.Message;
import org.kohsuke.args4j.Option;
@@ -62,12 +70,48 @@
@Option(name = "--no-auto-start", usage = "Don't automatically start daemon after init")
private boolean noAutoStart;
+ @Option(name = "--skip-plugins", usage = "Don't install plugin")
+ private boolean skipPlugins = false;
+
+ @Option(name = "--list-plugins", usage = "List available plugins")
+ private boolean listPlugins;
+
+ @Option(name = "--install-plugin", usage = "Install given plugin without asking", multiValued = true)
+ private List<String> installPlugins;
+
+ public Init() {
+ }
+
+ public Init(File sitePath) {
+ super(sitePath);
+ batchMode = true;
+ noAutoStart = true;
+ }
+
@Override
public int run() throws Exception {
ErrorLogFile.errorOnlyConsole();
final SiteInit init = createSiteInit();
+ if (!skipPlugins) {
+ final List<PluginData> plugins = InitPlugins.listPlugins(init.site);
+ ConsoleUI ui = ConsoleUI.getInstance(false);
+ verifyInstallPluginList(ui, plugins);
+ if (listPlugins) {
+ if (!plugins.isEmpty()) {
+ ui.message("Available plugins:\n");
+ for (PluginData plugin : plugins) {
+ ui.message(" * %s version %s\n", plugin.name, plugin.version);
+ }
+ } else {
+ ui.message("No plugins found.\n");
+ }
+ return 0;
+ }
+ }
+
init.flags.autoStart = !noAutoStart && init.site.isNew;
+ init.flags.skipPlugins = skipPlugins;
final SiteRun run;
try {
@@ -120,6 +164,10 @@
protected void configure() {
bind(ConsoleUI.class).toInstance(ui);
bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
+ List<String> plugins =
+ Objects.firstNonNull(installPlugins, Lists.<String> newArrayList());
+ bind(new TypeLiteral<List<String>>() {}).annotatedWith(
+ InstallPlugins.class).toInstance(plugins);
}
});
@@ -306,4 +354,26 @@
System.err.println("warn: Cannot remove " + path);
}
}
+
+ private void verifyInstallPluginList(ConsoleUI ui, List<PluginData> plugins) {
+ if (nullOrEmpty(installPlugins) || nullOrEmpty(plugins)) {
+ return;
+ }
+ ArrayList<String> copy = Lists.newArrayList(installPlugins);
+ List<String> pluginNames = Lists.transform(plugins, new Function<PluginData, String>() {
+ @Override
+ public String apply(PluginData input) {
+ return input.name;
+ }
+ });
+ copy.removeAll(pluginNames);
+ if (!copy.isEmpty()) {
+ ui.message("Cannot find plugin(s): %s\n", Joiner.on(", ").join(copy));
+ listPlugins = true;
+ }
+ }
+
+ private static boolean nullOrEmpty(List<?> list) {
+ return list == null || list.isEmpty();
+ }
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
new file mode 100644
index 0000000..f740fba
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
@@ -0,0 +1,212 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm;
+
+import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.lucene.LuceneIndexModule;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.cache.CacheRemovalListener;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
+import com.google.gerrit.server.index.ChangeBatchIndexer;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.IndexModule.IndexType;
+import com.google.gerrit.server.index.NoIndexModule;
+import com.google.gerrit.server.patch.PatchListCacheImpl;
+import com.google.gerrit.solr.SolrIndexModule;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.TypeLiteral;
+
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.util.io.NullOutputStream;
+import org.kohsuke.args4j.Option;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+public class Reindex extends SiteProgram {
+ @Option(name = "--threads", usage = "Number of threads to use for indexing")
+ private int threads = Runtime.getRuntime().availableProcessors();
+
+ @Option(name = "--schema-version",
+ usage = "Schema version to reindex; default is most recent version")
+ private Integer version;
+
+ @Option(name = "--output", usage = "Prefix for output; path for local disk index, or prefix for remote index")
+ private String outputBase;
+
+ @Option(name = "--verbose", usage = "Output debug information for each change")
+ private boolean verbose;
+
+ @Option(name = "--dry-run", usage = "Dry run: don't write anything to index")
+ private boolean dryRun;
+
+ private Injector dbInjector;
+ private Injector sysInjector;
+ private ChangeIndex index;
+
+ @Override
+ public int run() throws Exception {
+ mustHaveValidSite();
+ dbInjector = createDbInjector(MULTI_USER);
+ if (IndexModule.getIndexType(dbInjector) == IndexType.SQL) {
+ throw die("index.type must be configured (or not SQL)");
+ }
+ if (version == null) {
+ version = ChangeSchemas.getLatest().getVersion();
+ }
+ LifecycleManager dbManager = new LifecycleManager();
+ dbManager.add(dbInjector);
+ dbManager.start();
+
+ sysInjector = createSysInjector();
+ LifecycleManager sysManager = new LifecycleManager();
+ sysManager.add(sysInjector);
+ sysManager.start();
+
+ index = sysInjector.getInstance(IndexCollection.class).getSearchIndex();
+ index.markReady(false);
+ index.deleteAll();
+ int result = indexAll();
+ index.markReady(true);
+
+ sysManager.stop();
+ dbManager.stop();
+ return result;
+ }
+
+ private Injector createSysInjector() {
+ List<Module> modules = Lists.newArrayList();
+ modules.add(PatchListCacheImpl.module());
+ AbstractModule changeIndexModule;
+ switch (IndexModule.getIndexType(dbInjector)) {
+ case LUCENE:
+ changeIndexModule = new LuceneIndexModule(version, threads, outputBase);
+ break;
+ case SOLR:
+ changeIndexModule = new SolrIndexModule(false, threads, outputBase);
+ break;
+ default:
+ changeIndexModule = new NoIndexModule();
+ }
+ modules.add(changeIndexModule);
+ modules.add(new ReviewDbModule());
+ modules.add(new AbstractModule() {
+ @SuppressWarnings("rawtypes")
+ @Override
+ protected void configure() {
+ // Plugins are not loaded and we're just running through each change
+ // once, so don't worry about cache removal.
+ bind(new TypeLiteral<DynamicSet<CacheRemovalListener>>() {})
+ .toInstance(DynamicSet.<CacheRemovalListener> emptySet());
+ install(new DefaultCacheFactory.Module());
+ }
+ });
+ return dbInjector.createChildInjector(modules);
+ }
+
+ private class ReviewDbModule extends LifecycleModule {
+ @Override
+ protected void configure() {
+ final SchemaFactory<ReviewDb> schema = dbInjector.getInstance(
+ Key.get(new TypeLiteral<SchemaFactory<ReviewDb>>() {}));
+ final List<ReviewDb> dbs = Collections.synchronizedList(
+ Lists.<ReviewDb> newArrayListWithCapacity(threads + 1));
+ final ThreadLocal<ReviewDb> localDb = new ThreadLocal<ReviewDb>();
+
+ bind(ReviewDb.class).toProvider(new Provider<ReviewDb>() {
+ @Override
+ public ReviewDb get() {
+ ReviewDb db = localDb.get();
+ if (db == null) {
+ try {
+ db = schema.open();
+ dbs.add(db);
+ localDb.set(db);
+ } catch (OrmException e) {
+ throw new ProvisionException("unable to open ReviewDb", e);
+ }
+ }
+ return db;
+ }
+ });
+ listener().toInstance(new LifecycleListener() {
+ @Override
+ public void start() {
+ // Do nothing.
+ }
+
+ @Override
+ public void stop() {
+ for (ReviewDb db : dbs) {
+ db.close();
+ }
+ }
+ });
+ }
+ }
+
+ private int indexAll() throws Exception {
+ ReviewDb db = sysInjector.getInstance(ReviewDb.class);
+ ProgressMonitor pm = new TextProgressMonitor();
+ pm.start(1);
+ pm.beginTask("Collecting projects", ProgressMonitor.UNKNOWN);
+ Set<Project.NameKey> projects = Sets.newTreeSet();
+ int changeCount = 0;
+ try {
+ for (Change change : db.changes().all()) {
+ changeCount++;
+ if (projects.add(change.getProject())) {
+ pm.update(1);
+ }
+ }
+ } finally {
+ db.close();
+ }
+ pm.endTask();
+
+ ChangeBatchIndexer batchIndexer =
+ sysInjector.getInstance(ChangeBatchIndexer.class);
+ ChangeBatchIndexer.Result result = batchIndexer.indexAll(
+ index, projects, projects.size(), changeCount, System.err,
+ verbose ? System.out : NullOutputStream.INSTANCE);
+ int n = result.doneCount() + result.failedCount();
+ double t = result.elapsed(TimeUnit.MILLISECONDS) / 1000d;
+ System.out.format("Reindexed %d changes in %.01fs (%.01f/s)\n", n, t, n/t);
+ return result.success() ? 0 : 1;
+ }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java
deleted file mode 100644
index 67f5c91..0000000
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.pgm;
-
-/** Policy for auto upgrading schema on server startup */
-public enum SchemaUpgradePolicy {
-
- /** Perform schema migration if necessary and prune unused objects */
- AUTO,
-
- /** Like AUTO but don't prune unused objects */
- AUTO_NO_PRUNE,
-
- /** No automatic schema upgrade */
- OFF
-}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
new file mode 100644
index 0000000..e086e6a
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HiddenErrorHandler.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.http.jetty;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Strings;
+import com.google.gwtexpui.server.CacheHeaders;
+
+import org.eclipse.jetty.http.HttpHeaders;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.AbstractHttpConnection;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+class HiddenErrorHandler extends ErrorHandler {
+ private static final Logger log = LoggerFactory.getLogger(HiddenErrorHandler.class);
+
+ public void handle(String target, Request baseRequest,
+ HttpServletRequest req, HttpServletResponse res) throws IOException {
+ AbstractHttpConnection conn = AbstractHttpConnection.getCurrentConnection();
+ conn.getRequest().setHandled(true);
+ try {
+ log(req);
+ } finally {
+ reply(conn, res);
+ }
+ }
+
+ private void reply(AbstractHttpConnection conn, HttpServletResponse res)
+ throws IOException {
+ byte[] msg = message(conn);
+ res.setHeader(HttpHeaders.CONTENT_TYPE, "text/plain; charset=ISO-8859-1");
+ res.setContentLength(msg.length);
+ try {
+ CacheHeaders.setNotCacheable(res);
+ } finally {
+ ServletOutputStream out = res.getOutputStream();
+ try {
+ out.write(msg);
+ } finally {
+ out.close();
+ }
+ }
+ }
+
+ private static byte[] message(AbstractHttpConnection conn) {
+ String msg = conn.getResponse().getReason();
+ if (msg == null)
+ msg = HttpStatus.getMessage(conn.getResponse().getStatus());
+ return msg.getBytes(Charsets.ISO_8859_1);
+ }
+
+ private static void log(HttpServletRequest req) {
+ Throwable err = (Throwable)req.getAttribute("javax.servlet.error.exception");
+ if (err != null) {
+ String uri = req.getRequestURI();
+ if (!Strings.isNullOrEmpty(req.getQueryString())) {
+ uri += "?" + req.getQueryString();
+ }
+ log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
+ }
+ }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
index 6f439d2..5ee335d 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
@@ -111,7 +111,7 @@
uri = uri + "?" + qs;
}
- if (user instanceof IdentifiedUser) {
+ if (user != null && user.isIdentifiedUser()) {
IdentifiedUser who = (IdentifiedUser) user;
if (who.getUserName() != null && !who.getUserName().isEmpty()) {
event.setProperty(P_USER, who.getUserName());
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 3a7a874..35df8d9 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -17,6 +17,8 @@
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
+import com.google.common.base.Objects;
+import com.google.common.io.ByteStreams;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.reviewdb.client.AuthType;
@@ -49,12 +51,16 @@
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InterruptedIOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
@@ -64,14 +70,24 @@
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
+import java.util.Properties;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.servlet.DispatcherType;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
@Singleton
public class JettyServer {
+ private static final Logger log = LoggerFactory.getLogger(JettyServer.class);
+
static class Lifecycle implements LifecycleListener {
private final JettyServer server;
@@ -177,6 +193,12 @@
if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType)) {
ssl.setNeedClientAuth(true);
+
+ File crl = getFile(cfg, "sslcrl", "etc/crl.pem");
+ if (crl.exists()) {
+ ssl.setCrlPath(crl.getAbsolutePath());
+ ssl.setValidatePeerCerts(true);
+ }
}
defaultPort = 443;
@@ -296,7 +318,7 @@
final List<ContextHandler> all = new ArrayList<ContextHandler>();
for (String path : paths) {
- all.add(makeContext(path, env));
+ all.add(makeContext(path, env, cfg));
}
if (all.size() == 1) {
@@ -316,13 +338,14 @@
}
private ContextHandler makeContext(final String contextPath,
- final JettyEnv env) throws MalformedURLException, IOException {
+ final JettyEnv env, final Config cfg) throws MalformedURLException, IOException {
final ServletContextHandler app = new ServletContextHandler();
// This enables the use of sessions in Jetty, feature available
// for Gerrit plug-ins to enable user-level sessions.
//
app.setSessionHandler(new SessionHandler());
+ app.setErrorHandler(new HiddenErrorHandler());
// This is the path we are accessed by clients within our domain.
//
@@ -332,7 +355,28 @@
// need to unpack them into yet another temporary directory prior to
// serving to clients.
//
- app.setBaseResource(getBaseResource());
+ app.setBaseResource(getBaseResource(app));
+
+ // HTTP front-end filter to be used as surrogate of Apache HTTP
+ // reverse-proxy filtering.
+ // It is meant to be used as simpler tiny deployment of custom-made
+ // security enforcement (Security tokens, IP-based security filtering, others)
+ String filterClassName = cfg.getString("httpd", null, "filterClass");
+ if (filterClassName != null) {
+ try {
+ @SuppressWarnings("unchecked")
+ Class<? extends Filter> filterClass =
+ (Class<? extends Filter>) Class.forName(filterClassName);
+ Filter filter = env.webInjector.getInstance(filterClass);
+ app.addFilter(new FilterHolder(filter), "/*",
+ EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC));
+ } catch (Throwable e) {
+ String errorMessage =
+ "Unable to instantiate front-end HTTP Filter " + filterClassName;
+ log.error(errorMessage, e);
+ throw new IllegalArgumentException(errorMessage, e);
+ }
+ }
// Perform the same binding as our web.xml would do, but instead
// of using the listener to create the injector pass the one we
@@ -365,13 +409,14 @@
return app;
}
- private Resource getBaseResource() throws IOException {
+ private Resource getBaseResource(ServletContextHandler app)
+ throws IOException {
if (baseResource == null) {
try {
- baseResource = unpackWar();
+ baseResource = unpackWar(GerritLauncher.getDistributionArchive());
} catch (FileNotFoundException err) {
if (err.getMessage() == GerritLauncher.NOT_ARCHIVED) {
- baseResource = useDeveloperBuild();
+ baseResource = useDeveloperBuild(app);
} else {
throw err;
}
@@ -380,9 +425,13 @@
return baseResource;
}
- private Resource unpackWar() throws IOException {
- final File srcwar = GerritLauncher.getDistributionArchive();
+ private static Resource unpackWar(File srcwar) throws IOException {
+ File dstwar = makeWarTempDir();
+ unpack(srcwar, dstwar);
+ return Resource.newResource(dstwar.toURI());
+ }
+ private static File makeWarTempDir() throws IOException {
// Obtain our local temporary directory, but it comes back as a file
// so we have to switch it to be a directory post creation.
//
@@ -395,11 +444,13 @@
// a security feature. Try to resolve out any symlinks in the path.
//
try {
- dstwar = dstwar.getCanonicalFile();
+ return dstwar.getCanonicalFile();
} catch (IOException e) {
- dstwar = dstwar.getAbsoluteFile();
+ return dstwar.getAbsoluteFile();
}
+ }
+ private static void unpack(File srcwar, File dstwar) throws IOException {
final ZipFile zf = new ZipFile(srcwar);
try {
final Enumeration<? extends ZipEntry> e = zf.entries();
@@ -436,11 +487,9 @@
} finally {
zf.close();
}
-
- return Resource.newResource(dstwar.toURI());
}
- private void mkdir(final File dir) throws IOException {
+ private static void mkdir(File dir) throws IOException {
if (!dir.isDirectory()) {
mkdir(dir.getParentFile());
if (!dir.mkdir())
@@ -449,7 +498,8 @@
}
}
- private Resource useDeveloperBuild() throws IOException {
+ private Resource useDeveloperBuild(ServletContextHandler app)
+ throws IOException {
// Find ourselves in the CLASSPATH. We should be a loose class file.
//
URL u = getClass().getResource(getClass().getSimpleName() + ".class");
@@ -474,15 +524,119 @@
dir = dir.getParentFile();
}
- // We should be in a Maven style output, that is $jar/target/classes.
- //
if (!dir.getName().equals("classes")) {
throw new FileNotFoundException("Cannot find web root from " + u);
}
dir = dir.getParentFile(); // pop classes
- if (!dir.getName().equals("target")) {
+
+ if ("buck-out".equals(dir.getName())) {
+ final File dstwar = makeWarTempDir();
+ String pkg = "gerrit-gwtui";
+ String target = targetForBrowser(System.getProperty("gerrit.browser"));
+ final File gen = new File(dir, "gen");
+ String out = new File(new File(gen, pkg), target).getAbsolutePath();
+ final File zip = new File(out + ".zip");
+ final File root = dir.getParentFile();
+ final String name = "//" + pkg + ":" + target;
+
+ File ui = new File(dstwar, "gerrit_ui");
+ File p = new File(ui, "permutations");
+ mkdir(ui);
+ p.createNewFile();
+ p.deleteOnExit();
+
+ app.addFilter(new FilterHolder(new Filter() {
+ private long last;
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse res,
+ FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest req = (HttpServletRequest) request;
+ build(root, gen, name);
+ if (last != zip.lastModified()) {
+ last = zip.lastModified();
+ unpack(zip, dstwar);
+ }
+ chain.doFilter(req, res);
+ }
+
+ @Override
+ public void init(FilterConfig config) {
+ }
+ @Override
+ public void destroy() {
+ }
+ }), "/", EnumSet.of(DispatcherType.REQUEST));
+ return Resource.newResource(dstwar.toURI());
+ } else if ("target".equals(dir.getName())) {
+ return useMavenDeveloperBuild(dir);
+ } else {
throw new FileNotFoundException("Cannot find web root from " + u);
}
+ }
+
+ private static String targetForBrowser(String browser) {
+ if (browser == null || browser.isEmpty()) {
+ return "ui_dbg";
+ } else if (browser.startsWith("ui_")) {
+ return browser;
+ } else {
+ return "ui_" + browser;
+ }
+ }
+
+ private static void build(File root, File gen, String target)
+ throws IOException {
+ log.info("buck build " + target);
+ Properties properties = loadBuckProperties(gen);
+ String buck = Objects.firstNonNull(properties.getProperty("buck"), "buck");
+ ProcessBuilder proc = new ProcessBuilder(buck, "build", target)
+ .directory(root)
+ .redirectErrorStream(true);
+ if (properties.contains("PATH")) {
+ proc.environment().put("PATH", properties.getProperty("PATH"));
+ }
+ long start = System.currentTimeMillis();
+ Process rebuild = proc.start();
+ byte[] out;
+ InputStream in = rebuild.getInputStream();
+ try {
+ out = ByteStreams.toByteArray(in);
+ } finally {
+ rebuild.getOutputStream().close();
+ in.close();
+ }
+
+ int status;
+ try {
+ status = rebuild.waitFor();
+ } catch (InterruptedException e) {
+ throw new InterruptedIOException("interrupted waiting for " + buck);
+ }
+ if (status != 0) {
+ System.err.write(out);
+ System.err.println();
+ System.exit(status);
+ }
+
+ long time = System.currentTimeMillis() - start;
+ log.info(String.format("UPDATED %s in %.3fs", target, time / 1000.0));
+ }
+
+ private static Properties loadBuckProperties(File gen)
+ throws FileNotFoundException, IOException {
+ Properties properties = new Properties();
+ InputStream in = new FileInputStream(
+ new File(new File(gen, "tools"), "buck.properties"));
+ try {
+ properties.load(in);
+ } finally {
+ in.close();
+ }
+ return properties;
+ }
+
+ private Resource useMavenDeveloperBuild(File dir) throws IOException {
dir = dir.getParentFile(); // pop target
dir = dir.getParentFile(); // pop the module we are in
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
index 8d320d4..7730fa5 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
@@ -227,7 +227,7 @@
String userName = "";
CurrentUser who = userProvider.get();
- if (who instanceof IdentifiedUser) {
+ if (who.isIdentifiedUser()) {
String name = ((IdentifiedUser) who).getUserName();
if (name != null && !name.isEmpty()) {
userName = " (" + name + ")";
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
index 8e3948e..96ab103 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
@@ -39,12 +39,15 @@
}
public void open(final String link) throws Exception {
- String url = cfg.getString("httpd", null, "listenUrl");
+ String url = cfg.getString("gerrit", null, "canonicalWebUrl");
if (url == null) {
- return;
- }
- if (url.startsWith("proxy-")) {
- url = url.substring("proxy-".length());
+ url = cfg.getString("httpd", null, "listenUrl");
+ if (url == null) {
+ return;
+ }
+ if (url.startsWith("proxy-")) {
+ url = url.substring("proxy-".length());
+ }
}
final URI uri;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
index 32f8c2e..55419c2 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
@@ -36,6 +36,8 @@
bind(DatabaseConfigInitializer.class).annotatedWith(
Names.named("mysql")).to(MySqlInitializer.class);
bind(DatabaseConfigInitializer.class).annotatedWith(
+ Names.named("oracle")).to(OracleInitializer.class);
+ bind(DatabaseConfigInitializer.class).annotatedWith(
Names.named("postgresql")).to(PostgreSQLInitializer.class);
}
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
index aa413d6..2120a73 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
@@ -78,7 +78,9 @@
Names.named(dbType.toLowerCase())));
if (dci instanceof MySqlInitializer) {
- libraries.mysqlDriver.downloadRequired();
+ libraries.mysqlDriver.downloadRequired();
+ } else if (dci instanceof OracleInitializer) {
+ libraries.oracleDriver.downloadRequired();
}
dci.initConfig(database);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
index 5d71b48..267b41a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
@@ -23,6 +23,7 @@
import org.eclipse.jgit.util.FS;
import java.io.IOException;
+import java.util.List;
/** Global variables used by the 'init' command. */
@Singleton
@@ -33,11 +34,19 @@
/** Run the daemon (and open the web UI in a browser) after initialization. */
public boolean autoStart;
+ /** Skip plugins */
+ public boolean skipPlugins;
+
public final FileBasedConfig cfg;
public final FileBasedConfig sec;
+ public final List<String> installPlugins;
+
@Inject
- InitFlags(final SitePaths site) throws IOException, ConfigInvalidException {
+ InitFlags(final SitePaths site,
+ final @InstallPlugins List<String> installPlugins) throws IOException,
+ ConfigInvalidException {
+ this.installPlugins = installPlugins;
cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
sec = new FileBasedConfig(site.secure_config, FS.DETECTED);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
index b82df64..ee0d799 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
@@ -14,6 +14,7 @@
package com.google.gerrit.pgm.init;
+import com.google.common.collect.Lists;
import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.pgm.util.ConsoleUI;
import com.google.gerrit.server.config.SitePaths;
@@ -22,10 +23,10 @@
import com.google.inject.Singleton;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
+import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@@ -34,17 +35,64 @@
@Singleton
public class InitPlugins implements InitStep {
- private final static String PLUGIN_DIR = "WEB-INF/plugins/";
- private final static String JAR = ".jar";
+ private static final String PLUGIN_DIR = "WEB-INF/plugins/";
+ private static final String JAR = ".jar";
+
+ public static class PluginData {
+ public final String name;
+ public final String version;
+ public final File pluginFile;
+
+ private PluginData(String name, String version, File pluginFile) {
+ this.name = name;
+ this.version = version;
+ this.pluginFile = pluginFile;
+ }
+ }
+
+ public static List<PluginData> listPlugins(SitePaths site) throws IOException {
+ final File myWar = GerritLauncher.getDistributionArchive();
+ final List<PluginData> result = Lists.newArrayList();
+ try {
+ final ZipFile zf = new ZipFile(myWar);
+ try {
+ final Enumeration<? extends ZipEntry> e = zf.entries();
+ while (e.hasMoreElements()) {
+ final ZipEntry ze = e.nextElement();
+ if (ze.isDirectory()) {
+ continue;
+ }
+
+ if (ze.getName().startsWith(PLUGIN_DIR) && ze.getName().endsWith(JAR)) {
+ final String pluginJarName = new File(ze.getName()).getName();
+ final String pluginName = pluginJarName.substring(0, pluginJarName.length() - JAR.length());
+ final InputStream in = zf.getInputStream(ze);
+ final File tmpPlugin = PluginLoader.storeInTemp(pluginName, in, site);
+ final String pluginVersion = getVersion(tmpPlugin);
+
+ result.add(new PluginData(pluginName, pluginVersion, tmpPlugin));
+ }
+ }
+ } finally {
+ zf.close();
+ }
+ } catch (IOException e) {
+ throw new IOException("Failure during plugin installation", e);
+ }
+ return result;
+ }
private final ConsoleUI ui;
private final SitePaths site;
- private InitPluginStepsLoader pluginLoader;
+ private final InitFlags initFlags;
+ private final InitPluginStepsLoader pluginLoader;
@Inject
- InitPlugins(final ConsoleUI ui, final SitePaths site, InitPluginStepsLoader pluginLoader) {
+ InitPlugins(final ConsoleUI ui, final SitePaths site,
+ InitFlags initFlags, InitPluginStepsLoader pluginLoader) {
this.ui = ui;
this.site = site;
+ this.initFlags = initFlags;
this.pluginLoader = pluginLoader;
}
@@ -57,79 +105,44 @@
}
private void installPlugins() throws IOException {
- final File myWar;
- try {
- myWar = GerritLauncher.getDistributionArchive();
- } catch (FileNotFoundException e) {
- System.err.println("warn: Cannot find gerrit.war");
- return;
- }
-
- boolean foundPlugin = false;
- try {
- final ZipFile zf = new ZipFile(myWar);
+ List<PluginData> plugins = listPlugins(site);
+ for (PluginData plugin : plugins) {
+ String pluginName = plugin.name;
try {
- final Enumeration<? extends ZipEntry> e = zf.entries();
- while (e.hasMoreElements()) {
- final ZipEntry ze = e.nextElement();
- if (ze.isDirectory()) {
+ final File tmpPlugin = plugin.pluginFile;
+
+ if (!(initFlags.installPlugins.contains(pluginName) || ui.yesno(false,
+ "Install plugin %s version %s", pluginName, plugin.version))) {
+ tmpPlugin.delete();
+ continue;
+ }
+
+ final File p = new File(site.plugins_dir, plugin.name + ".jar");
+ if (p.exists()) {
+ final String installedPluginVersion = getVersion(p);
+ if (!ui.yesno(false,
+ "version %s is already installed, overwrite it",
+ installedPluginVersion)) {
+ tmpPlugin.delete();
continue;
}
-
- if (ze.getName().startsWith(PLUGIN_DIR) && ze.getName().endsWith(JAR)) {
- if (!foundPlugin) {
- if (!ui.yesno(false, "Prompt to install core plugins")) {
- return;
- }
- foundPlugin = true;
- }
-
- final String pluginJarName = new File(ze.getName()).getName();
- final String pluginName = pluginJarName.substring(0, pluginJarName.length() - JAR.length());
-
- final InputStream in = zf.getInputStream(ze);
- try {
- final File tmpPlugin = PluginLoader.storeInTemp(pluginName, in, site);
- final String pluginVersion = getVersion(tmpPlugin);
-
- if (!ui.yesno(false, "Install plugin %s version %s", pluginName,
- pluginVersion)) {
- tmpPlugin.delete();
- continue;
- }
-
- final File plugin = new File(site.plugins_dir, pluginJarName);
- if (plugin.exists()) {
- final String installedPluginVersion = getVersion(plugin);
- if (!ui.yesno(false,
- "version %s is already installed, overwrite it",
- installedPluginVersion)) {
- tmpPlugin.delete();
- continue;
- }
- if (!plugin.delete()) {
- throw new IOException("Failed to delete plugin " + pluginName
- + ": " + plugin.getAbsolutePath());
- }
- }
- if (!tmpPlugin.renameTo(plugin)) {
- throw new IOException("Failed to install plugin " + pluginName
- + ": " + tmpPlugin.getAbsolutePath() + " -> "
- + plugin.getAbsolutePath());
- }
- } finally {
- in.close();
- }
+ if (!p.delete()) {
+ throw new IOException("Failed to delete plugin " + pluginName
+ + ": " + p.getAbsolutePath());
}
}
+ if (!tmpPlugin.renameTo(p)) {
+ throw new IOException("Failed to install plugin " + pluginName
+ + ": " + tmpPlugin.getAbsolutePath() + " -> "
+ + p.getAbsolutePath());
+ }
} finally {
- zf.close();
+ if (plugin.pluginFile.exists()) {
+ plugin.pluginFile.delete();
+ }
}
- } catch (IOException e) {
- throw new IOException("Failure during plugin installation", e);
}
-
- if (!foundPlugin) {
+ if (plugins.isEmpty()) {
ui.message("No plugins found.");
}
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java
new file mode 100644
index 0000000..73db6f5
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InstallPlugins.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@BindingAnnotation
+@Retention(RetentionPolicy.RUNTIME)
+public @interface InstallPlugins {
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
index b1fa0c3..a03144b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
@@ -39,6 +39,7 @@
/* final */LibraryDownloader bouncyCastle;
/* final */LibraryDownloader mysqlDriver;
+ /* final */LibraryDownloader oracleDriver;
@Inject
Libraries(final Provider<LibraryDownloader> downloadProvider) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
index bf358f4..7caf49a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
@@ -15,6 +15,7 @@
package com.google.gerrit.pgm.init;
import com.google.common.base.Strings;
+import com.google.common.io.Files;
import com.google.gerrit.pgm.util.ConsoleUI;
import com.google.gerrit.pgm.util.Die;
import com.google.gerrit.pgm.util.IoUtil;
@@ -35,6 +36,7 @@
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.ProxySelector;
+import java.net.URISyntaxException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -50,6 +52,7 @@
private String sha1;
private String remove;
private File dst;
+ private boolean download; // download or copy
@Inject
LibraryDownloader(ConsoleUI ui, SitePaths site) {
@@ -63,6 +66,7 @@
void setJarUrl(final String url) {
this.jarUrl = url;
+ download = jarUrl.startsWith("http");
}
void setSHA1(final String sha1) {
@@ -117,7 +121,8 @@
msg.append(" If available, Gerrit can take advantage of features\n");
msg.append(" in the library, but will also function without it.\n");
}
- msg.append("Download and install it now");
+ msg.append(String.format(
+ "%s and install it now", download ? "Download" : "Copy"));
return ui.yesno(true, msg.toString(), name);
}
}
@@ -129,7 +134,11 @@
try {
removeStaleVersions();
- doGetByHttp();
+ if (download) {
+ doGetByHttp();
+ } else {
+ doGetByLocalCopy();
+ }
verifyFileChecksum();
} catch (IOException err) {
dst.delete();
@@ -186,6 +195,28 @@
}
}
+ private void doGetByLocalCopy() throws IOException {
+ System.err.print("Copying " + jarUrl + " ...");
+ File f = url2file(jarUrl);
+ if (!f.exists()) {
+ StringBuilder msg = new StringBuilder()
+ .append("\n")
+ .append("Can not find the %s at this location: %s\n")
+ .append("Please provide alternative URL");
+ f = url2file(ui.readString(null, msg.toString(), name, jarUrl));
+ }
+ Files.copy(f, dst);
+ }
+
+ private static File url2file(final String urlString) throws IOException {
+ final URL url = new URL(urlString);
+ try {
+ return new File(url.toURI());
+ } catch (URISyntaxException e) {
+ return new File(url.getPath());
+ }
+ }
+
private void doGetByHttp() throws IOException {
System.err.print("Downloading " + jarUrl + " ...");
System.err.flush();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/OracleInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/OracleInitializer.java
new file mode 100644
index 0000000..180beb0
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/OracleInitializer.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import static com.google.gerrit.pgm.init.InitUtil.username;
+
+
+public class OracleInitializer implements DatabaseConfigInitializer {
+
+ @Override
+ public void initConfig(Section databaseSection) {
+ final String defPort = "1521";
+ databaseSection.string("Server hostname", "hostname", "localhost");
+ databaseSection.string("Server port", "port", defPort, false);
+ databaseSection.string("Instance name", "instance", "xe");
+ databaseSection.string("Database username", "username", username());
+ databaseSection.password("username", "password");
+ }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
index 387b93a..6489035 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
@@ -96,7 +96,7 @@
final boolean nullIfDefault) {
final String ov = get(name);
String nv = ui.readString(ov != null ? ov : dv, "%s", title);
- if (nullIfDefault && nv == dv) {
+ if (nullIfDefault && nv.equals(dv)) {
nv = null;
}
if (!eq(ov, nv)) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index bf0af7f..1c2d024 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -75,6 +75,10 @@
mkdir(site.data_dir);
for (InitStep step : steps) {
+ if (step instanceof InitPlugins
+ && flags.skipPlugins) {
+ continue;
+ }
step.run();
}
@@ -91,10 +95,10 @@
extractMailExample("Comment.vm");
extractMailExample("CommentFooter.vm");
extractMailExample("CommitMessageEdited.vm");
+ extractMailExample("Footer.vm");
extractMailExample("Merged.vm");
extractMailExample("MergeFail.vm");
extractMailExample("NewChange.vm");
- extractMailExample("RebasedPatchSet.vm");
extractMailExample("RegisterNewEmail.vm");
extractMailExample("ReplacePatchSet.vm");
extractMailExample("Restored.vm");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java
index 833b4d8..af65170 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java
@@ -16,15 +16,12 @@
import com.google.gerrit.util.cli.CmdLineParser;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import com.google.inject.Module;
+import com.google.gerrit.util.cli.OptionHandlers;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.Option;
import java.io.StringWriter;
-import java.util.Collections;
/** Base class for command line invocations of Gerrit Code Review. */
public abstract class AbstractProgram {
@@ -44,8 +41,7 @@
}
public final int main(final String[] argv) throws Exception {
- final Injector empty = emptyInjector();
- final CmdLineParser clp = new CmdLineParser(empty, this);
+ final CmdLineParser clp = new CmdLineParser(OptionHandlers.empty(), this);
try {
clp.parseArgument(argv);
} catch (CmdLineException err) {
@@ -81,10 +77,6 @@
}
}
- private static Injector emptyInjector() {
- return Guice.createInjector(Collections.<Module> emptyList());
- }
-
/** Create a new exception to indicate we won't continue. */
protected static Die die(String why) {
return new Die(why);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
index aae5b48..f4f0bd2 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -51,6 +51,13 @@
@Option(name = "--site-path", aliases = {"-d"}, usage = "Local directory containing site data")
private File sitePath = new File(".");
+ protected SiteProgram() {
+ }
+
+ protected SiteProgram(File sitePath) {
+ this.sitePath = sitePath;
+ }
+
/** @return the site path specified on the command line. */
protected File getSitePath() {
File path = sitePath.getAbsoluteFile();
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
index f1ecadd..6f80364 100644
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
@@ -13,6 +13,7 @@
# limitations under the License.
+# Version should match lib/bouncycastle/BUCK
[library "bouncyCastle"]
name = Bouncy Castle Crypto v144
url = http://www.bouncycastle.org/download/bcprov-jdk16-144.jar
@@ -24,3 +25,9 @@
url = http://repo2.maven.org/maven2/mysql/mysql-connector-java/5.1.21/mysql-connector-java-5.1.21.jar
sha1 = 7abbd19fc2e2d5b92c0895af8520f7fa30266be9
remove = mysql-connector-java-.*[.]jar
+
+[library "oracleDriver"]
+ name = Oracle JDBC driver 11g Release 2 (11.2.0)
+ url = file:///u01/app/oracle/product/11.2.0/xe/jdbc/lib/ojdbc6.jar
+ sha1 = 2f89cd9176772c3a6c261ce6a8e3d0d4425f5679
+ remove = ojdbc6.jar
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
index 86f0260..26c4382 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
@@ -37,6 +37,7 @@
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
+import java.util.Collections;
public class UpgradeFrom2_0_xTest extends InitTestCase {
@@ -68,7 +69,8 @@
old.setString("sendemail", null, "smtpPass", "email.s3kr3t");
old.save();
- final InitFlags flags = new InitFlags(site);
+ final InitFlags flags =
+ new InitFlags(site, Collections.<String> emptyList());
final ConsoleUI ui = createStrictMock(ConsoleUI.class);
Section.Factory sections = new Section.Factory() {
@Override
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
deleted file mode 100644
index 10df196..0000000
--- a/gerrit-plugin-api/pom.xml
+++ /dev/null
@@ -1,122 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2012 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-plugin-api</artifactId>
- <name>Gerrit Code Review - Plugin API</name>
-
- <description>
- API for tightly coupled plugins to compile against
- </description>
-
- <dependencies>
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-sshd</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-httpd</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-pgm</artifactId>
- <version>${project.version}</version>
- <exclusions>
- <exclusion>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-servlet</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>org.apache.tomcat</groupId>
- <artifactId>servlet-api</artifactId>
- </dependency>
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-shade-plugin</artifactId>
- <configuration>
- <createSourcesJar>true</createSourcesJar>
- <artifactSet>
- <excludes>
- <exclude>gwtjsonrpc:gwtjsonrpc</exclude>
- <exclude>com.google.gerrit:gerrit-gwtexpui</exclude>
- <exclude>com.google.gerrit:gerrit-prettify</exclude>
- <exclude>com.google.gerrit:gerrit-patch-commonsnet</exclude>
- <exclude>com.google.gerrit:gerrit-patch-jgit</exclude>
- <exclude>com.google.gerrit:gerrit-util-ssl</exclude>
-
- <exclude>com.googlecode.juniversalchardet:juniversalchardet</exclude>
- <exclude>com.googlecode.prolog-cafe:PrologCafe</exclude>
- <exclude>org.slf4j:slf4j-log4j12</exclude>
- <exclude>log4j:log4j</exclude>
-
- <exclude>commons-collections:commons-collections</exclude>
- <exclude>commons-codec:commons-codec</exclude>
- <exclude>commons-dbcp:commons-dbcp</exclude>
- <exclude>commons-lang:commons-lang</exclude>
- <exclude>commons-net:commons-net</exclude>
- <exclude>commons-pool:commons-pool</exclude>
-
- <exclude>asm:asm</exclude>
- <exclude>eu.medsea.mimeutil:mime-util</exclude>
- <exclude>org.antlr:antlr</exclude>
- <exclude>org.antlr:antlr-runtime</exclude>
- <exclude>org.apache.mina:mina-core</exclude>
- <exclude>oro:oro</exclude>
- </excludes>
- </artifactSet>
- <filters>
- <filter>
- <artifact>com.google.gerrit:gerrit-server</artifact>
- <excludes>
- <exclude>gerrit/**</exclude>
- </excludes>
- </filter>
- </filters>
- </configuration>
- <executions>
- <execution>
- <phase>package</phase>
- <goals>
- <goal>shade</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index f721be4..1b33510 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -18,13 +18,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
+ <groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-archetype</artifactId>
+ <version>2.8-SNAPSHOT</version>
<name>Gerrit Code Review - Plugin Archetype</name>
<properties>
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
index 92099fa..2b8dfb2 100644
--- a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
@@ -94,9 +94,9 @@
<repository>
<id>gerrit-api-repository</id>
#if ($gerritApiVersion.endsWith("SNAPSHOT"))
- <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
+ <url>https://gerrit-api.storage.googleapis.com/snapshot/</url>
#else
- <url>https://gerrit-api.commondatastorage.googleapis.com/release/</url>
+ <url>https://gerrit-api.storage.googleapis.com/release/</url>
#end
</repository>
</repositories>
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index 9ea0cd0..3c4dc99 100644
--- a/gerrit-plugin-gwt-archetype/pom.xml
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -18,13 +18,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
+ <groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-gwt-archetype</artifactId>
+ <version>2.8-SNAPSHOT</version>
<name>Gerrit Code Review - Web Ui GWT Plugin Archetype</name>
<properties>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
index 6e1ba21..ddfdac0 100644
--- a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
@@ -120,9 +120,9 @@
<repository>
<id>gerrit-api-repository</id>
#if ($gerritApiVersion.endsWith("SNAPSHOT"))
- <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
+ <url>https://gerrit-api.storage.googleapis.com/snapshot/</url>
#else
- <url>https://gerrit-api.commondatastorage.googleapis.com/release/</url>
+ <url>https://gerrit-api.storage.googleapis.com/release/</url>
#end
</repository>
</repositories>
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
deleted file mode 100644
index 3d1b923..0000000
--- a/gerrit-plugin-gwtui/pom.xml
+++ /dev/null
@@ -1,125 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2012 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-plugin-gwtui</artifactId>
- <name>Gerrit Code Review - Plugin GWT UI</name>
-
- <description>
- API for UI plugins to build with GWT and integrate with Gerrit
- </description>
-
- <dependencies>
- <dependency>
- <groupId>com.google.gwt</groupId>
- <artifactId>gwt-user</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.gwt</groupId>
- <artifactId>gwt-dev</artifactId>
- </dependency>
- </dependencies>
-
- <build>
- <pluginManagement>
- <plugins>
- <plugin>
- <groupId>org.eclipse.m2e</groupId>
- <artifactId>lifecycle-mapping</artifactId>
- <version>1.0.0</version>
- <configuration>
- <lifecycleMappingMetadata>
- <pluginExecutions>
- <pluginExecution>
- <pluginExecutionFilter>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>gwt-maven-plugin</artifactId>
- <versionRange>[2.5.0,)</versionRange>
- <goals>
- <goal>resources</goal>
- <goal>compile</goal>
- <goal>i18n</goal>
- <goal>generateAsync</goal>
- </goals>
- </pluginExecutionFilter>
- <action>
- <execute />
- </action>
- </pluginExecution>
- <pluginExecution>
- <pluginExecutionFilter>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-war-plugin</artifactId>
- <versionRange>[2.1.1,)</versionRange>
- <goals>
- <goal>exploded</goal>
- </goals>
- </pluginExecutionFilter>
- <action>
- <execute />
- </action>
- </pluginExecution>
- </pluginExecutions>
- </lifecycleMappingMetadata>
- </configuration>
- </plugin>
- </plugins>
- </pluginManagement>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-source-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>jar</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
-
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>gwt-maven-plugin</artifactId>
- <configuration>
- <module>com.google.gerrit.Plugin</module>
- <disableClassMetadata>true</disableClassMetadata>
- <disableCastChecking>true</disableCastChecking>
- </configuration>
- <executions>
- <execution>
- <goals>
- <goal>resources</goal>
- <goal>compile</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
-
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index 2972a8e..681d7a9 100644
--- a/gerrit-plugin-js-archetype/pom.xml
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -18,13 +18,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
+ <groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-js-archetype</artifactId>
+ <version>2.8-SNAPSHOT</version>
<name>Gerrit Code Review - Web UI JavaScript Plugin Archetype</name>
<properties>
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
index 2a8b469..814fc5e 100644
--- a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
@@ -112,9 +112,9 @@
<repository>
<id>gerrit-api-repository</id>
#if ($gerritApiVersion.endsWith("SNAPSHOT"))
- <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
+ <url>https://gerrit-api.storage.googleapis.com/snapshot/</url>
#else
- <url>https://gerrit-api.commondatastorage.googleapis.com/release/</url>
+ <url>https://gerrit-api.storage.googleapis.com/release/</url>
#end
</repository>
</repositories>
diff --git a/gerrit-prettify/BUCK b/gerrit-prettify/BUCK
new file mode 100644
index 0000000..79dc760
--- /dev/null
+++ b/gerrit-prettify/BUCK
@@ -0,0 +1,47 @@
+SRC = 'src/main/java/com/google/gerrit/prettify/'
+
+gwt_module(
+ name = 'client',
+ srcs = glob([
+ SRC + 'client/**/*.java',
+ SRC + 'common/**/*.java',
+ ]),
+ gwtxml = SRC + 'PrettyFormatter.gwt.xml',
+ resources = glob([
+ 'src/main/java/com/google/gerrit/prettify/client/*.properties',
+ ]),
+ deps = [
+ ':google-code-prettify',
+ '//gerrit-patch-jgit:client',
+ '//gerrit-reviewdb:client',
+ '//gerrit-gwtexpui:SafeHtml',
+ '//lib:guava',
+ '//lib:gwtjsonrpc',
+ '//lib/gwt:user',
+ '//lib/jgit:jgit',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+java_library(
+ name = 'google-code-prettify',
+ resources = glob([
+ 'src/main/resources/com/google/gerrit/prettify/client/**/*',
+ ]),
+ deps = [
+ '//lib:LICENSE-Apache2.0',
+ ],
+)
+
+java_library(
+ name = 'server',
+ srcs = glob([SRC + 'common/**/*.java']),
+ deps = [
+ '//gerrit-patch-jgit:server',
+ '//gerrit-reviewdb:server',
+ '//lib:guava',
+ '//lib:gwtjsonrpc',
+ '//lib/jgit:jgit',
+ ],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
deleted file mode 100644
index fafc399..0000000
--- a/gerrit-prettify/pom.xml
+++ /dev/null
@@ -1,76 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-prettify</artifactId>
- <name>Gerrit Code Review - Prettify</name>
-
- <description>
- Prettify based syntax highlighting
- </description>
-
- <dependencies>
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-gwtexpui</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-patch-jgit</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-reviewdb</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gwt</groupId>
- <artifactId>gwt-user</artifactId>
- <scope>provided</scope>
- </dependency>
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-source-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>jar</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/gerrit-reviewdb/BUCK b/gerrit-reviewdb/BUCK
new file mode 100644
index 0000000..05674cf
--- /dev/null
+++ b/gerrit-reviewdb/BUCK
@@ -0,0 +1,20 @@
+SRC = 'src/main/java/com/google/gerrit/reviewdb/'
+
+gwt_module(
+ name = 'client',
+ srcs = glob([SRC + 'client/**/*.java']),
+ gwtxml = SRC + 'ReviewDB.gwt.xml',
+ deps = [
+ '//lib:gwtorm',
+ '//lib:gwtorm_src'
+ ],
+ visibility = ['PUBLIC'],
+)
+
+java_library(
+ name = 'server',
+ srcs = glob([SRC + '**/*.java']),
+ resources = glob(['src/main/resources/**/*']),
+ deps = ['//lib:gwtorm'],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
deleted file mode 100644
index 5543c53..0000000
--- a/gerrit-reviewdb/pom.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-reviewdb</artifactId>
- <name>Gerrit Code Review - ReviewDB</name>
-
- <description>
- Database schema definition and interface.
- </description>
-
- <dependencies>
- <dependency>
- <groupId>gwtorm</groupId>
- <artifactId>gwtorm</artifactId>
- </dependency>
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-source-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>jar</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
index 94b37e1..221ba46 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
@@ -228,4 +228,14 @@
public void setUserName(final String userName) {
this.userName = userName;
}
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof Account && ((Account) o).getId().equals(getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return getId().get();
+ }
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
index ad0f130..92c479e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
@@ -27,7 +27,7 @@
/** Preferred scheme type to download a change. */
public static enum DownloadScheme {
- ANON_GIT, ANON_HTTP, ANON_SSH, HTTP, SSH, REPO_DOWNLOAD, DEFAULT_DOWNLOADS;
+ ANON_GIT, ANON_HTTP, HTTP, SSH, REPO_DOWNLOAD, DEFAULT_DOWNLOADS;
}
/** Preferred method to download a change. */
@@ -72,6 +72,11 @@
EXPAND_ALL;
}
+ public static enum DiffView {
+ SIDE_BY_SIDE,
+ UNIFIED_DIFF
+ }
+
public static enum TimeFormat {
/** 12-hour clock: 1:15 am, 2:13 pm */
HHMM_12("h:mm a"),
@@ -128,7 +133,7 @@
protected boolean reversePatchSetOrder;
@Column(id = 11)
- protected boolean showUsernameInReviewCategory;
+ protected boolean showUserInReview;
@Column(id = 12)
protected boolean relativeDateInChangeTable;
@@ -136,6 +141,9 @@
@Column(id = 13, length = 20, notNull = false)
protected String commentVisibilityStrategy;
+ @Column(id = 14, length = 20, notNull = false)
+ protected String diffView;
+
public AccountGeneralPreferences() {
}
@@ -210,11 +218,11 @@
}
public boolean isShowUsernameInReviewCategory() {
- return showUsernameInReviewCategory;
+ return showUserInReview;
}
public void setShowUsernameInReviewCategory(final boolean showUsernameInReviewCategory) {
- this.showUsernameInReviewCategory = showUsernameInReviewCategory;
+ this.showUserInReview = showUsernameInReviewCategory;
}
public DateFormat getDateFormat() {
@@ -249,7 +257,7 @@
public CommentVisibilityStrategy getCommentVisibilityStrategy() {
if (commentVisibilityStrategy == null) {
- return CommentVisibilityStrategy.EXPAND_MOST_RECENT;
+ return CommentVisibilityStrategy.EXPAND_RECENT;
}
return CommentVisibilityStrategy.valueOf(commentVisibilityStrategy);
}
@@ -259,17 +267,30 @@
commentVisibilityStrategy = strategy.name();
}
+ public DiffView getDiffView() {
+ if (diffView == null) {
+ return DiffView.SIDE_BY_SIDE;
+ }
+ return DiffView.valueOf(diffView);
+ }
+
+ public void setDiffView(DiffView diffView) {
+ this.diffView = diffView.name();
+ }
+
public void resetToDefaults() {
maximumPageSize = DEFAULT_PAGESIZE;
showSiteHeader = true;
useFlashClipboard = true;
copySelfOnEmail = false;
reversePatchSetOrder = false;
- showUsernameInReviewCategory = false;
+ showUserInReview = false;
downloadUrl = null;
downloadCommand = null;
dateFormat = null;
timeFormat = null;
relativeDateInChangeTable = false;
+ commentVisibilityStrategy = null;
+ diffView = null;
}
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupById.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
new file mode 100644
index 0000000..3443f80
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.CompoundKey;
+
+/** Membership of an {@link AccountGroup} in an {@link AccountGroup}. */
+public final class AccountGroupById {
+ public static class Key extends CompoundKey<AccountGroup.Id> {
+ private static final long serialVersionUID = 1L;
+
+ @Column(id = 1)
+ protected AccountGroup.Id groupId;
+
+ @Column(id = 2)
+ protected AccountGroup.UUID includeUUID;
+
+ protected Key() {
+ groupId = new AccountGroup.Id();
+ includeUUID = new AccountGroup.UUID();
+ }
+
+ public Key(final AccountGroup.Id g, final AccountGroup.UUID u) {
+ groupId = g;
+ includeUUID = u;
+ }
+
+ @Override
+ public AccountGroup.Id getParentKey() {
+ return groupId;
+ }
+
+ public AccountGroup.Id getGroupId() {
+ return groupId;
+ }
+
+ public AccountGroup.UUID getIncludeUUID() {
+ return includeUUID;
+ }
+
+ @Override
+ public com.google.gwtorm.client.Key<?>[] members() {
+ return new com.google.gwtorm.client.Key<?>[] {includeUUID};
+ }
+ }
+
+ @Column(id = 1, name = Column.NONE)
+ protected Key key;
+
+ protected AccountGroupById() {
+ }
+
+ public AccountGroupById(final AccountGroupById.Key k) {
+ key = k;
+ }
+
+ public AccountGroupById.Key getKey() {
+ return key;
+ }
+
+ public AccountGroup.Id getGroupId() {
+ return key.groupId;
+ }
+
+ public AccountGroup.UUID getIncludeUUID() {
+ return key.includeUUID;
+ }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
new file mode 100644
index 0000000..8ea78e2
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
@@ -0,0 +1,115 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.CompoundKey;
+
+import java.sql.Timestamp;
+
+/** Inclusion of an {@link AccountGroup} in another {@link AccountGroup}. */
+public final class AccountGroupByIdAud {
+ public static class Key extends CompoundKey<AccountGroup.Id> {
+ private static final long serialVersionUID = 1L;
+
+ @Column(id = 1)
+ protected AccountGroup.Id groupId;
+
+ @Column(id = 2)
+ protected AccountGroup.UUID includeUUID;
+
+ @Column(id = 3)
+ protected Timestamp addedOn;
+
+ protected Key() {
+ groupId = new AccountGroup.Id();
+ includeUUID = new AccountGroup.UUID();
+ }
+
+ public Key(final AccountGroup.Id g, final AccountGroup.UUID u, final Timestamp t) {
+ groupId = g;
+ includeUUID = u;
+ addedOn = t;
+ }
+
+ @Override
+ public AccountGroup.Id getParentKey() {
+ return groupId;
+ }
+
+ public AccountGroup.UUID getIncludeUUID() {
+ return includeUUID;
+ }
+
+ public Timestamp getAddedOn() {
+ return addedOn;
+ }
+
+ @Override
+ public com.google.gwtorm.client.Key<?>[] members() {
+ return new com.google.gwtorm.client.Key<?>[] {includeUUID};
+ }
+ }
+
+ @Column(id = 1, name = Column.NONE)
+ protected Key key;
+
+ @Column(id = 2)
+ protected Account.Id addedBy;
+
+ @Column(id = 3, notNull = false)
+ protected Account.Id removedBy;
+
+ @Column(id = 4, notNull = false)
+ protected Timestamp removedOn;
+
+ protected AccountGroupByIdAud() {
+ }
+
+ public AccountGroupByIdAud(final AccountGroupById m,
+ final Account.Id adder, final Timestamp when) {
+ final AccountGroup.Id group = m.getGroupId();
+ final AccountGroup.UUID include = m.getIncludeUUID();
+ key = new AccountGroupByIdAud.Key(group, include, when);
+ addedBy = adder;
+ }
+
+ public AccountGroupByIdAud(final AccountGroupById m,
+ final Account.Id adder) {
+ this(m, adder, now());
+ }
+
+ public AccountGroupByIdAud.Key getKey() {
+ return key;
+ }
+
+ public boolean isActive() {
+ return removedOn == null;
+ }
+
+ public void removed(final Account.Id deleter) {
+ removedBy = deleter;
+ removedOn = now();
+ }
+
+ public void removed(final Account.Id deleter, final Timestamp when) {
+ removedBy = deleter;
+ removedOn = when;
+ }
+
+ private static Timestamp now() {
+ return new Timestamp(System.currentTimeMillis());
+ }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuid.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuid.java
deleted file mode 100644
index a5b35ed..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuid.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-
-/** Membership of an {@link AccountGroup} in an {@link AccountGroup}. */
-public final class AccountGroupIncludeByUuid {
- public static class Key extends CompoundKey<AccountGroup.Id> {
- private static final long serialVersionUID = 1L;
-
- @Column(id = 1)
- protected AccountGroup.Id groupId;
-
- @Column(id = 2)
- protected AccountGroup.UUID includeUUID;
-
- protected Key() {
- groupId = new AccountGroup.Id();
- includeUUID = new AccountGroup.UUID();
- }
-
- public Key(final AccountGroup.Id g, final AccountGroup.UUID u) {
- groupId = g;
- includeUUID = u;
- }
-
- @Override
- public AccountGroup.Id getParentKey() {
- return groupId;
- }
-
- public AccountGroup.Id getGroupId() {
- return groupId;
- }
-
- public AccountGroup.UUID getIncludeUUID() {
- return includeUUID;
- }
-
- @Override
- public com.google.gwtorm.client.Key<?>[] members() {
- return new com.google.gwtorm.client.Key<?>[] {includeUUID};
- }
- }
-
- @Column(id = 1, name = Column.NONE)
- protected Key key;
-
- protected AccountGroupIncludeByUuid() {
- }
-
- public AccountGroupIncludeByUuid(final AccountGroupIncludeByUuid.Key k) {
- key = k;
- }
-
- public AccountGroupIncludeByUuid.Key getKey() {
- return key;
- }
-
- public AccountGroup.Id getGroupId() {
- return key.groupId;
- }
-
- public AccountGroup.UUID getIncludeUUID() {
- return key.includeUUID;
- }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuidAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuidAudit.java
deleted file mode 100644
index 6625197..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuidAudit.java
+++ /dev/null
@@ -1,115 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-
-import java.sql.Timestamp;
-
-/** Inclusion of an {@link AccountGroup} in another {@link AccountGroup}. */
-public final class AccountGroupIncludeByUuidAudit {
- public static class Key extends CompoundKey<AccountGroup.Id> {
- private static final long serialVersionUID = 1L;
-
- @Column(id = 1)
- protected AccountGroup.Id groupId;
-
- @Column(id = 2)
- protected AccountGroup.UUID includeUUID;
-
- @Column(id = 3)
- protected Timestamp addedOn;
-
- protected Key() {
- groupId = new AccountGroup.Id();
- includeUUID = new AccountGroup.UUID();
- }
-
- public Key(final AccountGroup.Id g, final AccountGroup.UUID u, final Timestamp t) {
- groupId = g;
- includeUUID = u;
- addedOn = t;
- }
-
- @Override
- public AccountGroup.Id getParentKey() {
- return groupId;
- }
-
- public AccountGroup.UUID getIncludeUUID() {
- return includeUUID;
- }
-
- public Timestamp getAddedOn() {
- return addedOn;
- }
-
- @Override
- public com.google.gwtorm.client.Key<?>[] members() {
- return new com.google.gwtorm.client.Key<?>[] {includeUUID};
- }
- }
-
- @Column(id = 1, name = Column.NONE)
- protected Key key;
-
- @Column(id = 2)
- protected Account.Id addedBy;
-
- @Column(id = 3, notNull = false)
- protected Account.Id removedBy;
-
- @Column(id = 4, notNull = false)
- protected Timestamp removedOn;
-
- protected AccountGroupIncludeByUuidAudit() {
- }
-
- public AccountGroupIncludeByUuidAudit(final AccountGroupIncludeByUuid m,
- final Account.Id adder, final Timestamp when) {
- final AccountGroup.Id group = m.getGroupId();
- final AccountGroup.UUID include = m.getIncludeUUID();
- key = new AccountGroupIncludeByUuidAudit.Key(group, include, when);
- addedBy = adder;
- }
-
- public AccountGroupIncludeByUuidAudit(final AccountGroupIncludeByUuid m,
- final Account.Id adder) {
- this(m, adder, now());
- }
-
- public AccountGroupIncludeByUuidAudit.Key getKey() {
- return key;
- }
-
- public boolean isActive() {
- return removedOn == null;
- }
-
- public void removed(final Account.Id deleter) {
- removedBy = deleter;
- removedOn = now();
- }
-
- public void removed(final Account.Id deleter, final Timestamp when) {
- removedBy = deleter;
- removedOn = when;
- }
-
- private static Timestamp now() {
- return new Timestamp(System.currentTimeMillis());
- }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 2a51872..7dedf14 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -431,6 +431,10 @@
lastUpdatedOn = now;
}
+ public int getRowVersion() {
+ return rowVersion;
+ }
+
public void resetLastUpdatedOn() {
lastUpdatedOn = new Timestamp(System.currentTimeMillis());
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java
new file mode 100644
index 0000000..b1b2615
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CommentRange.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import com.google.gwtorm.client.Column;
+
+public class CommentRange {
+
+ @Column(id = 1)
+ protected int startLine;
+
+ @Column(id = 2)
+ protected int startCharacter;
+
+ @Column(id = 3)
+ protected int endLine;
+
+ @Column(id = 4)
+ protected int endCharacter;
+
+ protected CommentRange() {
+ }
+
+ public CommentRange(int sl, int sc, int el, int ec) {
+ startLine = sl;
+ startCharacter = sc;
+ endLine = el;
+ endCharacter = ec;
+ }
+
+ public int getStartLine() {
+ return startLine;
+ }
+
+ public int getStartCharacter() {
+ return startCharacter;
+ }
+
+ public int getEndLine() {
+ return endLine;
+ }
+
+ public int getEndCharacter() {
+ return endCharacter;
+ }
+
+ public void setStartLine(int sl) {
+ startLine = sl;
+ }
+
+ public void setStartCharacter(int sc) {
+ startCharacter = sc;
+ }
+
+ public void setEndLine(int el) {
+ endLine = el;
+ }
+
+ public void setEndCharacter(int ec) {
+ endCharacter = ec;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof CommentRange) {
+ CommentRange other = (CommentRange) obj;
+ return startLine == other.startLine && startCharacter == other.startCharacter &&
+ endLine == other.endLine && endCharacter == other.endCharacter;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = startLine;
+ h = h * 31 + startCharacter;
+ h = h * 31 + endLine;
+ h = h * 31 + endCharacter;
+ return h;
+ }
+
+ @Override
+ public String toString() {
+ return "Range[startLine=" + startLine + ", startCharacter=" + startCharacter
+ + ", endLine=" + endLine + ", endCharacter=" + endCharacter + "]";
+ }
+}
\ No newline at end of file
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
index af35e52f..0119c3e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -117,6 +117,9 @@
@Column(id = 8, length = 40, notNull = false)
protected String parentUuid;
+ @Column(id = 9, notNull = false)
+ protected CommentRange range;
+
protected PatchLineComment() {
}
@@ -189,4 +192,12 @@
public void setParentUuid(String inReplyTo) {
parentUuid = inReplyTo;
}
+
+ public void setRange(CommentRange r) {
+ range = r;
+ }
+
+ public CommentRange getRange() {
+ return range;
+ }
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
index d243496..f3cf471 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
@@ -107,6 +107,8 @@
protected InheritableBoolean requireChangeID;
+ protected String maxObjectSizeLimit;
+
protected InheritableBoolean useContentMerge;
protected String defaultDashboardId;
@@ -160,6 +162,10 @@
return requireChangeID;
}
+ public String getMaxObjectSizeLimit() {
+ return maxObjectSizeLimit;
+ }
+
public void setUseContributorAgreements(final InheritableBoolean u) {
useContributorAgreements = u;
}
@@ -176,6 +182,10 @@
requireChangeID = cid;
}
+ public void setMaxObjectSizeLimit(final String limit) {
+ maxObjectSizeLimit = limit;
+ }
+
public SubmitType getSubmitType() {
return submitType;
}
@@ -224,6 +234,7 @@
requireChangeID = update.requireChangeID;
submitType = update.submitType;
state = update.state;
+ maxObjectSizeLimit = update.maxObjectSizeLimit;
}
/**
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
index d90a81c..c3a535b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
@@ -54,4 +54,14 @@
revEnd.append('z');
return new RevId(revEnd.toString());
}
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return (o instanceof RevId) && id.equals(((RevId) o).id);
+ }
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAccess.java
new file mode 100644
index 0000000..d1eaed8
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAccess.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.server;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
+
+public interface AccountGroupByIdAccess extends
+ Access<AccountGroupById, AccountGroupById.Key> {
+ @PrimaryKey("key")
+ AccountGroupById get(AccountGroupById.Key key) throws OrmException;
+
+ @Query("WHERE key.includeUUID = ?")
+ ResultSet<AccountGroupById> byIncludeUUID(AccountGroup.UUID uuid) throws OrmException;
+
+ @Query("WHERE key.groupId = ?")
+ ResultSet<AccountGroupById> byGroup(AccountGroup.Id id) throws OrmException;
+
+ @Query("")
+ ResultSet<AccountGroupById> all() throws OrmException;
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java
new file mode 100644
index 0000000..e772c8c
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupByIdAudAccess.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.server;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
+
+public interface AccountGroupByIdAudAccess extends
+ Access<AccountGroupByIdAud, AccountGroupByIdAud.Key> {
+ @PrimaryKey("key")
+ AccountGroupByIdAud get(AccountGroupByIdAud.Key key)
+ throws OrmException;
+
+ @Query("WHERE key.groupId = ? AND key.includeUUID = ?")
+ ResultSet<AccountGroupByIdAud> byGroupInclude(AccountGroup.Id groupId,
+ AccountGroup.UUID incGroupUUID) throws OrmException;
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAccess.java
deleted file mode 100644
index 50e23eb..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAccess.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountGroupIncludeByUuidAccess extends
- Access<AccountGroupIncludeByUuid, AccountGroupIncludeByUuid.Key> {
- @PrimaryKey("key")
- AccountGroupIncludeByUuid get(AccountGroupIncludeByUuid.Key key) throws OrmException;
-
- @Query("WHERE key.includeUUID = ?")
- ResultSet<AccountGroupIncludeByUuid> byIncludeUUID(AccountGroup.UUID uuid) throws OrmException;
-
- @Query("WHERE key.groupId = ?")
- ResultSet<AccountGroupIncludeByUuid> byGroup(AccountGroup.Id id) throws OrmException;
-
- @Query("")
- ResultSet<AccountGroupIncludeByUuid> all() throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAuditAccess.java
deleted file mode 100644
index 1c95f75..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAuditAccess.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountGroupIncludeByUuidAuditAccess extends
- Access<AccountGroupIncludeByUuidAudit, AccountGroupIncludeByUuidAudit.Key> {
- @PrimaryKey("key")
- AccountGroupIncludeByUuidAudit get(AccountGroupIncludeByUuidAudit.Key key)
- throws OrmException;
-
- @Query("WHERE key.groupId = ? AND key.includeUUID = ?")
- ResultSet<AccountGroupIncludeByUuidAudit> byGroupInclude(AccountGroup.Id groupId,
- AccountGroup.UUID incGroupUUID) throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
index 7e0b90c..703edbb 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
@@ -30,10 +30,10 @@
@Query("WHERE id.changeId = ? ORDER BY id.patchSetId")
ResultSet<PatchSet> byChange(Change.Id id) throws OrmException;
- @Query("WHERE revision = ? LIMIT 2")
+ @Query("WHERE revision = ?")
ResultSet<PatchSet> byRevision(RevId rev) throws OrmException;
- @Query("WHERE revision >= ? AND revision <= ? LIMIT 2")
+ @Query("WHERE revision >= ? AND revision <= ?")
ResultSet<PatchSet> byRevisionRange(RevId reva, RevId revb)
throws OrmException;
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
index f2b1cb7..ac2c849 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
@@ -14,6 +14,7 @@
package com.google.gerrit.reviewdb.server;
+import com.google.gerrit.reviewdb.client.Change.Id;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetAncestor;
import com.google.gerrit.reviewdb.client.RevId;
@@ -31,6 +32,9 @@
@Query("WHERE key.patchSetId = ? ORDER BY key.position")
ResultSet<PatchSetAncestor> ancestorsOf(PatchSet.Id id) throws OrmException;
+ @Query("WHERE key.patchSetId.changeId = ?")
+ ResultSet<PatchSetAncestor> byChange(Id id) throws OrmException;
+
@Query("WHERE key.patchSetId = ?")
ResultSet<PatchSetAncestor> byPatchSet(PatchSet.Id id) throws OrmException;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index f1ab752..1a6f817 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -105,10 +105,10 @@
SubmoduleSubscriptionAccess submoduleSubscriptions();
@Relation(id = 29)
- AccountGroupIncludeByUuidAccess accountGroupIncludesByUuid();
+ AccountGroupByIdAccess accountGroupById();
@Relation(id = 30)
- AccountGroupIncludeByUuidAuditAccess accountGroupIncludesByUuidAudit();
+ AccountGroupByIdAudAccess accountGroupByIdAud();
/** Create the next unique id for an {@link Account}. */
@Sequence(startWith = 1000000)
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index d6609e7..495f7b1 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -34,17 +34,16 @@
-- *********************************************************************
--- AccountGroupIncludeByUuidAccess
+-- AccountGroupByIdAccess
-- @PrimaryKey covers: byGroup
-CREATE INDEX account_group_includes_by_uuid_byInclude
-ON account_group_includes_by_uuid (include_uuid);
-
+CREATE INDEX account_group_id_byInclude
+ON account_group_by_id (include_uuid);
-- *********************************************************************
-- AccountProjectWatchAccess
-- @PrimaryKey covers: byAccount
-- covers: byProject
-CREATE INDEX account_project_watches_byProject
+CREATE INDEX account_project_watches_byP
ON account_project_watches (project_name);
@@ -114,7 +113,7 @@
ON patch_set_approvals (change_open, account_id);
-- covers: closedByUser
-CREATE INDEX patch_set_approvals_closedByUser
+CREATE INDEX patch_set_approvals_closedByU
ON patch_set_approvals (change_open, account_id, change_sort_key);
@@ -163,5 +162,5 @@
-- *********************************************************************
-- SubmoduleSubscriptionAccess
-CREATE INDEX submodule_subscription_access_bySubscription
+CREATE INDEX submodule_subscr_acc_byS
ON submodule_subscriptions (submodule_project_name, submodule_branch_name);
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index 3b62e84..5b8c463 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -82,17 +82,16 @@
-- *********************************************************************
--- AccountGroupIncludeByUuidAccess
+-- AccountGroupByIdAccess
-- @PrimaryKey covers: byGroup
-CREATE INDEX account_group_includes_by_uuid_byInclude
-ON account_group_includes_by_uuid (include_uuid);
-
+CREATE INDEX account_group_id_byInclude
+ON account_group_by_id (include_uuid);
-- *********************************************************************
-- AccountProjectWatchAccess
-- @PrimaryKey covers: byAccount
-- covers: byProject
-CREATE INDEX account_project_watches_byProject
+CREATE INDEX account_project_watches_byP
ON account_project_watches (project_name);
@@ -170,7 +169,7 @@
WHERE change_open = 'Y';
-- covers: closedByUser
-CREATE INDEX patch_set_approvals_closedByUser
+CREATE INDEX patch_set_approvals_closedByU
ON patch_set_approvals (account_id, change_sort_key)
WHERE change_open = 'N';
@@ -222,5 +221,5 @@
-- *********************************************************************
-- SubmoduleSubscriptionAccess
-CREATE INDEX submodule_subscription_access_bySubscription
+CREATE INDEX submodule_subscr_acc_byS
ON submodule_subscriptions (submodule_project_name, submodule_branch_name);
diff --git a/gerrit-server/.settings/org.eclipse.core.resources.prefs b/gerrit-server/.settings/org.eclipse.core.resources.prefs
index 29abf99..1daeba9 100644
--- a/gerrit-server/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-server/.settings/org.eclipse.core.resources.prefs
@@ -3,4 +3,5 @@
encoding//src/main/resources=UTF-8
encoding//src/test/java=UTF-8
encoding//src/test/resources=UTF-8
+encoding//target/generated-sources/prolog-java=UTF-8
encoding/<project>=UTF-8
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
new file mode 100644
index 0000000..1e20546
--- /dev/null
+++ b/gerrit-server/BUCK
@@ -0,0 +1,117 @@
+SRCS = glob(['src/main/java/**/*.java'])
+RESOURCES = glob(['src/main/resources/**/*'])
+
+# TODO(sop) break up gerrit-server java_library(), its too big
+java_library2(
+ name = 'server',
+ srcs = SRCS,
+ resources = RESOURCES,
+ deps = [
+ '//gerrit-antlr:query_exception',
+ '//gerrit-antlr:query_parser',
+ '//gerrit-common:server',
+ '//gerrit-extension-api:api',
+ '//gerrit-patch-commonsnet:commons-net',
+ '//gerrit-patch-jgit:server',
+ '//gerrit-prettify:server',
+ '//gerrit-reviewdb:server',
+ '//gerrit-util-cli:cli',
+ '//gerrit-util-ssl:ssl',
+ '//lib:args4j',
+ '//lib:automaton',
+ '//lib:gson',
+ '//lib:guava',
+ '//lib:gwtjsonrpc',
+ '//lib:gwtorm',
+ '//lib:jsch',
+ '//lib:jsr305',
+ '//lib:juniversalchardet',
+ '//lib:mime-util',
+ '//lib:ow2-asm',
+ '//lib:ow2-asm-tree',
+ '//lib:ow2-asm-util',
+ '//lib:parboiled-core',
+ '//lib:pegdown',
+ '//lib:protobuf',
+ '//lib:velocity',
+ '//lib/antlr:java_runtime',
+ '//lib/commons:codec',
+ '//lib/commons:dbcp',
+ '//lib/commons:lang',
+ '//lib/commons:net',
+ '//lib/guice:guice',
+ '//lib/guice:guice-assistedinject',
+ '//lib/guice:guice-servlet',
+ '//lib/jgit:jgit',
+ '//lib/log:api',
+ '//lib/prolog:prolog-cafe',
+ ],
+ compile_deps = [
+ '//lib/bouncycastle:bcprov',
+ '//lib/bouncycastle:bcpg',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+java_sources(
+ name = 'server-src',
+ srcs = SRCS + RESOURCES,
+ visibility = ['PUBLIC'],
+)
+
+PROLOG_TEST_CASE = [
+ 'src/test/java/com/google/gerrit/rules/PrologTestCase.java',
+]
+PROLOG_TESTS = glob(
+ ['src/test/java/com/google/gerrit/rules/**/*.java'],
+ excludes = PROLOG_TEST_CASE,
+)
+
+java_library(
+ name = 'prolog_test_case',
+ srcs = PROLOG_TEST_CASE,
+ deps = [
+ ':server',
+ '//lib:junit',
+ '//lib/guice:guice',
+ '//lib/prolog:prolog-cafe',
+ ],
+ export_deps = True,
+)
+
+java_test(
+ name = 'prolog_tests',
+ srcs = PROLOG_TESTS,
+ resources = glob(['src/test/resources/com/google/gerrit/rules/**/*']),
+ deps = [
+ ':prolog_test_case',
+ '//gerrit-server/src/main/prolog:common',
+ ],
+)
+
+java_test(
+ name = 'server_tests',
+ srcs = glob(
+ ['src/test/java/**/*.java'],
+ excludes = PROLOG_TESTS + PROLOG_TEST_CASE
+ ),
+ deps = [
+ ':server',
+ '//gerrit-antlr:query_exception',
+ '//gerrit-antlr:query_parser',
+ '//gerrit-common:server',
+ '//gerrit-extension-api:api',
+ '//gerrit-reviewdb:server',
+ '//lib:easymock',
+ '//lib:guava',
+ '//lib:gwtorm',
+ '//lib:h2',
+ '//lib:junit',
+ '//lib/antlr:java_runtime',
+ '//lib/guice:guice',
+ '//lib/jgit:jgit',
+ '//lib/jgit:junit',
+ '//lib/prolog:prolog-cafe',
+ ],
+ source_under_test = [':server'],
+)
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
deleted file mode 100644
index e543634..0000000
--- a/gerrit-server/pom.xml
+++ /dev/null
@@ -1,248 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-server</artifactId>
- <name>Gerrit Code Review - Server</name>
-
- <description>
- Commons server routines
- </description>
-
- <dependencies>
- <dependency>
- <groupId>log4j</groupId>
- <artifactId>log4j</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.apache.velocity</groupId>
- <artifactId>velocity</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit.junit</artifactId>
- </dependency>
-
- <dependency>
- <groupId>commons-dbcp</groupId>
- <artifactId>commons-dbcp</artifactId>
- </dependency>
-
- <dependency>
- <groupId>commons-lang</groupId>
- <artifactId>commons-lang</artifactId>
- </dependency>
-
- <dependency>
- <groupId>commons-net</groupId>
- <artifactId>commons-net</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- </dependency>
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-log4j12</artifactId>
- </dependency>
-
- <dependency>
- <groupId>bouncycastle</groupId>
- <artifactId>bcpg-jdk15</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>eu.medsea.mimeutil</groupId>
- <artifactId>mime-util</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.inject</groupId>
- <artifactId>guice</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.inject.extensions</groupId>
- <artifactId>guice-servlet</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.inject.extensions</groupId>
- <artifactId>guice-assistedinject</artifactId>
- </dependency>
-
- <dependency>
- <groupId>aopalliance</groupId>
- <artifactId>aopalliance</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-antlr</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-common</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-extension-api</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-util-cli</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-util-ssl</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-patch-commonsnet</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>test</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.code.findbugs</groupId>
- <artifactId>jsr305</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.googlecode.juniversalchardet</groupId>
- <artifactId>juniversalchardet</artifactId>
- </dependency>
-
- <dependency>
- <groupId>dk.brics.automaton</groupId>
- <artifactId>automaton</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.googlecode.prolog-cafe</groupId>
- <artifactId>PrologCafe</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.pegdown</groupId>
- <artifactId>pegdown</artifactId>
- </dependency>
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-antrun-plugin</artifactId>
- <executions>
- <execution>
- <id>prolog-to-java</id>
- <phase>generate-sources</phase>
- <goals>
- <goal>run</goal>
- </goals>
- <configuration>
- <target>
- <property name="gensrc" location="${project.build.directory}/generated-sources"/>
-
- <java classname="com.googlecode.prolog_cafe.compiler.Compiler"
- fork="true"
- failonerror="true"
- classpathref="maven.compile.classpath">
- <arg value="--show-stack-trace"/>
- <arg value="-O"/>
- <arg value="-am"/><arg value="${gensrc}/prolog-am"/>
- <arg value="-s" /><arg value="${gensrc}/prolog-java"/>
- <arg value="src/main/prolog/gerrit_common.pl"/>
- </java>
- </target>
- </configuration>
- </execution>
- </executions>
- </plugin>
-
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>build-helper-maven-plugin</artifactId>
- <executions>
- <execution>
- <id>add-source</id>
- <phase>generate-sources</phase>
- <goals>
- <goal>add-source</goal>
- </goals>
- <configuration>
- <sources>
- <source>${project.build.directory}/generated-sources/prolog-java</source>
- </sources>
- </configuration>
- </execution>
- </executions>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-source-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>jar</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 2d54601..9cf6c28 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -14,6 +14,7 @@
package com.google.gerrit.common;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
@@ -44,6 +45,7 @@
import com.google.gerrit.server.events.PatchSetCreatedEvent;
import com.google.gerrit.server.events.RefUpdatedEvent;
import com.google.gerrit.server.events.ReviewerAddedEvent;
+import com.google.gerrit.server.events.TopicChangedEvent;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.project.ProjectCache;
@@ -186,6 +188,9 @@
/** Filename of the reviewer added hook. */
private final File reviewerAddedHook;
+ /** Filename of the topic changed hook. */
+ private final File topicChangedHook;
+
/** Filename of the cla signed hook. */
private final File claSignedHook;
@@ -209,7 +214,7 @@
private final SitePaths sitePaths;
/** Thread pool used to monitor sync hooks */
- private final ExecutorService syncHookThreadPool = Executors.newCachedThreadPool();
+ private final ExecutorService syncHookThreadPool;
/** Timeout value for synchronous hooks */
private final int syncHookTimeout;
@@ -254,9 +259,14 @@
changeRestoredHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeRestoredHook", "change-restored")).getPath());
refUpdatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdatedHook", "ref-updated")).getPath());
reviewerAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "reviewerAddedHook", "reviewer-added")).getPath());
+ topicChangedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "topicChangedHook", "topic-changed")).getPath());
claSignedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "claSignedHook", "cla-signed")).getPath());
refUpdateHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdateHook", "ref-update")).getPath());
syncHookTimeout = config.getInt("hooks", "syncHookTimeout", 30);
+ syncHookThreadPool = Executors.newCachedThreadPool(
+ new ThreadFactoryBuilder()
+ .setNameFormat("SyncHook-%d")
+ .build());
}
public void addChangeListener(ChangeListener listener, IdentifiedUser user) {
@@ -469,11 +479,13 @@
}
public void doChangeAbandonedHook(final Change change, final Account account,
- final String reason, final ReviewDb db) throws OrmException {
+ final PatchSet patchSet, final String reason, final ReviewDb db)
+ throws OrmException {
final ChangeAbandonedEvent event = new ChangeAbandonedEvent();
event.change = eventFactory.asChangeAttribute(change);
event.abandoner = eventFactory.asAccountAttribute(account);
+ event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
event.reason = reason;
fireEvent(change, event, db);
@@ -484,17 +496,20 @@
addArg(args, "--branch", event.change.branch);
addArg(args, "--topic", event.change.topic);
addArg(args, "--abandoner", getDisplayName(account));
+ addArg(args, "--commit", event.patchSet.revision);
addArg(args, "--reason", reason == null ? "" : reason);
runHook(change.getProject(), changeAbandonedHook, args);
}
public void doChangeRestoredHook(final Change change, final Account account,
- final String reason, final ReviewDb db) throws OrmException {
+ final PatchSet patchSet, final String reason, final ReviewDb db)
+ throws OrmException {
final ChangeRestoredEvent event = new ChangeRestoredEvent();
event.change = eventFactory.asChangeAttribute(change);
event.restorer = eventFactory.asAccountAttribute(account);
+ event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
event.reason = reason;
fireEvent(change, event, db);
@@ -505,6 +520,7 @@
addArg(args, "--branch", event.change.branch);
addArg(args, "--topic", event.change.topic);
addArg(args, "--restorer", getDisplayName(account));
+ addArg(args, "--commit", event.patchSet.revision);
addArg(args, "--reason", reason == null ? "" : reason);
runHook(change.getProject(), changeRestoredHook, args);
@@ -554,6 +570,25 @@
runHook(change.getProject(), reviewerAddedHook, args);
}
+ public void doTopicChangedHook(final Change change, final Account account,
+ final String oldTopic, final ReviewDb db)
+ throws OrmException {
+ final TopicChangedEvent event = new TopicChangedEvent();
+
+ event.change = eventFactory.asChangeAttribute(change);
+ event.changer = eventFactory.asAccountAttribute(account);
+ event.oldTopic = oldTopic;
+ fireEvent(change, event, db);
+
+ final List<String> args = new ArrayList<String>();
+ addArg(args, "--change", event.change.id);
+ addArg(args, "--changer", getDisplayName(account));
+ addArg(args, "--old-topic", oldTopic);
+ addArg(args, "--new-topic", event.change.topic);
+
+ runHook(change.getProject(), topicChangedHook, args);
+ }
+
public void doClaSignupHook(Account account, ContributorAgreement cla) {
if (account != null) {
final List<String> args = new ArrayList<String>();
@@ -776,23 +811,25 @@
}
}
- final int exitValue = result.getExitValue();
- if (exitValue == 0) {
- log.debug("hook[" + getName() + "] exitValue:" + exitValue);
- } else {
- log.info("hook[" + getName() + "] exitValue:" + exitValue);
- }
-
- BufferedReader br =
- new BufferedReader(new StringReader(result.getOutput()));
- try {
- String line;
- while ((line = br.readLine()) != null) {
- log.info("hook[" + getName() + "] output: " + line);
+ if (result != null) {
+ final int exitValue = result.getExitValue();
+ if (exitValue == 0) {
+ log.debug("hook[" + getName() + "] exitValue:" + exitValue);
+ } else {
+ log.info("hook[" + getName() + "] exitValue:" + exitValue);
}
- }
- catch(IOException iox) {
- log.error("Error writing hook output", iox);
+
+ BufferedReader br =
+ new BufferedReader(new StringReader(result.getOutput()));
+ try {
+ String line;
+ while ((line = br.readLine()) != null) {
+ log.info("hook[" + getName() + "] output: " + line);
+ }
+ }
+ catch(IOException iox) {
+ log.error("Error writing hook output", iox);
+ }
}
return result;
@@ -838,7 +875,7 @@
}
}
- /** Runable type used to run async hooks */
+ /** Runnable type used to run asynchronous hooks */
private final class AsyncHookTask extends HookTask implements Runnable {
private AsyncHookTask(Project.NameKey project, File hook, List<String> args) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index 48a52a0..28c64a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -103,7 +103,7 @@
* @throws OrmException
*/
public void doChangeAbandonedHook(Change change, Account account,
- String reason, ReviewDb db) throws OrmException;
+ PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
/**
* Fire the Change Restored Hook.
@@ -114,7 +114,7 @@
* @throws OrmException
*/
public void doChangeRestoredHook(Change change, Account account,
- String reason, ReviewDb db) throws OrmException;
+ PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
/**
* Fire the Ref Updated Hook
@@ -147,6 +147,16 @@
public void doReviewerAddedHook(Change change, Account account,
PatchSet patchSet, ReviewDb db) throws OrmException;
+ /**
+ * Fire the Topic Changed Hook
+ *
+ * @param change The change itself.
+ * @param account The gerrit user who changed the topic.
+ * @param oldTopic The old topic name.
+ */
+ public void doTopicChangedHook(Change change, Account account,
+ String oldTopic, ReviewDb db) throws OrmException;
+
public void doClaSignupHook(Account account, ContributorAgreement cla);
/**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index 6011ab0..021d1d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -37,7 +37,7 @@
@Override
public void doChangeAbandonedHook(Change change, Account account,
- String reason, ReviewDb db) {
+ PatchSet patchSet, String reason, ReviewDb db) {
}
@Override
@@ -52,7 +52,7 @@
@Override
public void doChangeRestoredHook(Change change, Account account,
- String reason, ReviewDb db) {
+ PatchSet patchSet, String reason, ReviewDb db) {
}
@Override
@@ -91,6 +91,11 @@
}
@Override
+ public void doTopicChangedHook(Change change, Account account, String oldTopic,
+ ReviewDb db) {
+ }
+
+ @Override
public void removeChangeListener(ChangeListener listener) {
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
index 310b401..234a0e7b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
@@ -14,8 +14,15 @@
package com.google.gerrit.rules;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
-import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
@@ -56,23 +63,22 @@
PrologEnvironment create(PrologMachineCopy src);
}
- private final Injector injector;
+ private final Args args;
private final Map<StoredValue<Object>, Object> storedValues;
private List<Runnable> cleanup;
@Inject
- PrologEnvironment(Injector i, @Assisted PrologMachineCopy src) {
+ PrologEnvironment(Args a, @Assisted PrologMachineCopy src) {
super(src);
- injector = i;
setMaxArity(MAX_ARITY);
setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
+ args = a;
storedValues = new HashMap<StoredValue<Object>, Object>();
cleanup = new LinkedList<Runnable>();
}
- /** Get the global Guice Injector that configured the environment. */
- public Injector getInjector() {
- return injector;
+ public Args getArgs() {
+ return args;
}
/**
@@ -139,4 +145,53 @@
i.remove();
}
}
-}
\ No newline at end of file
+
+ @Singleton
+ public static class Args {
+ private final ProjectCache projectCache;
+ private final GitRepositoryManager repositoryManager;
+ private final PatchListCache patchListCache;
+ private final PatchSetInfoFactory patchSetInfoFactory;
+ private final IdentifiedUser.GenericFactory userFactory;
+ private final Provider<AnonymousUser> anonymousUser;
+
+ @Inject
+ Args(ProjectCache projectCache,
+ GitRepositoryManager repositoryManager,
+ PatchListCache patchListCache,
+ PatchSetInfoFactory patchSetInfoFactory,
+ IdentifiedUser.GenericFactory userFactory,
+ Provider<AnonymousUser> anonymousUser) {
+ this.projectCache = projectCache;
+ this.repositoryManager = repositoryManager;
+ this.patchListCache = patchListCache;
+ this.patchSetInfoFactory = patchSetInfoFactory;
+ this.userFactory = userFactory;
+ this.anonymousUser = anonymousUser;
+ }
+
+ public ProjectCache getProjectCache() {
+ return projectCache;
+ }
+
+ public GitRepositoryManager getGitRepositoryManager() {
+ return repositoryManager;
+ }
+
+ public PatchListCache getPatchListCache() {
+ return patchListCache;
+ }
+
+ public PatchSetInfoFactory getPatchSetInfoFactory() {
+ return patchSetInfoFactory;
+ }
+
+ public IdentifiedUser.GenericFactory getUserFactory() {
+ return userFactory;
+ }
+
+ public AnonymousUser getAnonymousUser() {
+ return anonymousUser.get();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
index 92b8b1b..74a3928 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
@@ -20,7 +20,15 @@
public class PrologModule extends FactoryModule {
@Override
protected void configure() {
- DynamicSet.setOf(binder(), PredicateProvider.class);
- factory(PrologEnvironment.Factory.class);
+ install(new EnvironmentModule());
+ bind(PrologEnvironment.Args.class);
+ }
+
+ static class EnvironmentModule extends FactoryModule {
+ @Override
+ protected void configure() {
+ DynamicSet.setOf(binder(), PredicateProvider.class);
+ factory(PrologEnvironment.Factory.class);
+ }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 1185fd3..2dbd33b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -60,7 +60,7 @@
PatchSet ps = StoredValues.PATCH_SET.get(engine);
PrologEnvironment env = (PrologEnvironment) engine.control;
PatchSetInfoFactory patchInfoFactory =
- env.getInjector().getInstance(PatchSetInfoFactory.class);
+ env.getArgs().getPatchSetInfoFactory();
try {
return patchInfoFactory.get(change, ps);
} catch (PatchSetInfoNotAvailableException e) {
@@ -74,7 +74,7 @@
public PatchList createValue(Prolog engine) {
PrologEnvironment env = (PrologEnvironment) engine.control;
PatchSetInfo psInfo = StoredValues.PATCH_SET_INFO.get(engine);
- PatchListCache plCache = env.getInjector().getInstance(PatchListCache.class);
+ PatchListCache plCache = env.getArgs().getPatchListCache();
Change change = StoredValues.CHANGE.get(engine);
Project.NameKey projectKey = change.getProject();
ObjectId a = null;
@@ -95,8 +95,7 @@
@Override
public Repository createValue(Prolog engine) {
PrologEnvironment env = (PrologEnvironment) engine.control;
- GitRepositoryManager gitMgr =
- env.getInjector().getInstance(GitRepositoryManager.class);
+ GitRepositoryManager gitMgr = env.getArgs().getGitRepositoryManager();
Change change = StoredValues.CHANGE.get(engine);
Project.NameKey projectKey = change.getProject();
final Repository repo;
@@ -122,7 +121,7 @@
@Override
protected AnonymousUser createValue(Prolog engine) {
PrologEnvironment env = (PrologEnvironment) engine.control;
- return env.getInjector().getInstance(AnonymousUser.class);
+ return env.getArgs().getAnonymousUser();
}
};
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 8f73014..f79e05f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -14,26 +14,25 @@
package com.google.gerrit.server;
+import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy.RECEIVE_COMMITS;
+
import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetAncestor;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.client.TrackingId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeMessages;
+import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.config.TrackingFooter;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.mail.CommitMessageEditedSender;
@@ -42,11 +41,9 @@
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.RefControl;
import com.google.gerrit.server.util.IdGenerator;
import com.google.gerrit.server.util.MagicBranch;
-import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmConcurrencyException;
import com.google.gwtorm.server.OrmException;
@@ -64,6 +61,8 @@
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.util.ChangeIdUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.sql.Timestamp;
@@ -78,11 +77,15 @@
import java.util.regex.Matcher;
public class ChangeUtil {
+ private static final long SORT_KEY_EPOCH = 1222819200L; // Oct 1 2008 00:00
private static final Object uuidLock = new Object();
private static final int SEED = 0x2418e6f9;
private static int uuidPrefix;
private static int uuidSeq;
+ private static final Logger log =
+ LoggerFactory.getLogger(ChangeUtil.class);
+
/**
* Generate a new unique identifier for change message entities.
*
@@ -116,6 +119,15 @@
}
}
+ public static void bumpRowVersionNotLastUpdatedOn(Change.Id id, ReviewDb db)
+ throws OrmException {
+ // Empty update of Change to bump rowVersion, changing its ETag.
+ Change c = db.changes().get(id);
+ if (c != null) {
+ db.changes().update(Collections.singleton(c));
+ }
+ }
+
public static void updated(final Change c) {
c.resetLastUpdatedOn();
computeSortKey(c);
@@ -174,11 +186,6 @@
db.trackingIds().delete(toDelete);
}
- public static void testMerge(MergeOp.Factory opFactory, Change change)
- throws NoSuchProjectException {
- opFactory.create(change.getDest()).verifyMergeability(change);
- }
-
public static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src)
throws OrmException {
final int cnt = src.getParentCount();
@@ -197,7 +204,7 @@
ReviewDb db, RevertedSender.Factory revertedSenderFactory,
ChangeHooks hooks, Repository git,
PatchSetInfoFactory patchSetInfoFactory, PersonIdent myIdent,
- ChangeInserter changeInserter)
+ ChangeInserter.Factory changeInserterFactory)
throws NoSuchChangeException, EmailException,
OrmException, MissingObjectException, IncorrectObjectTypeException,
IOException, InvalidChangeOperationException {
@@ -223,7 +230,7 @@
revertCommitBuilder.addParentId(commitToRevert);
revertCommitBuilder.setTreeId(parentToCommitToRevert.getTree());
revertCommitBuilder.setAuthor(authorIdent);
- revertCommitBuilder.setCommitter(myIdent);
+ revertCommitBuilder.setCommitter(authorIdent);
if (message == null) {
message = MessageFormat.format(
@@ -252,18 +259,14 @@
user.getAccountId(),
changeToRevert.getDest());
change.setTopic(changeToRevert.getTopic());
-
- PatchSet.Id id =
- new PatchSet.Id(change.getId(), Change.INITIAL_PATCH_SET_ID);
- final PatchSet ps = new PatchSet(id);
- ps.setCreatedOn(change.getCreatedOn());
- ps.setUploader(change.getOwner());
- ps.setRevision(new RevId(revertCommit.name()));
+ ChangeInserter ins =
+ changeInserterFactory.create(refControl, change, revertCommit);
+ PatchSet ps = ins.getPatchSet();
String ref = refControl.getRefName();
final String cmdRef =
MagicBranch.NEW_PUBLISH_CHANGE
- + ref.substring(ref.lastIndexOf("/") + 1);
+ + ref.substring(ref.lastIndexOf('/') + 1);
CommitReceivedEvent commitReceivedEvent =
new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
revertCommit.getId(), cmdRef), refControl.getProjectControl()
@@ -275,11 +278,6 @@
throw new InvalidChangeOperationException(e.getMessage());
}
- PatchSetInfo info = patchSetInfoFactory.get(revertCommit, ps.getId());
- change.setCurrentPatchSet(info);
- ChangeUtil.updated(change);
-
-
final RefUpdate ru = git.updateRef(ps.getRefName());
ru.setExpectedOldObjectId(ObjectId.zeroId());
ru.setNewObjectId(revertCommit);
@@ -299,14 +297,17 @@
msgBuf.append("This patchset was reverted in change: " + change.getKey().get());
cmsg.setMessage(msgBuf.toString());
- LabelTypes labelTypes = refControl.getProjectControl().getLabelTypes();
- changeInserter.insertChange(db, change, cmsg, ps, revertCommit,
- labelTypes, info, Collections.<Account.Id> emptySet());
+ ins.setMessage(cmsg).insert();
- final RevertedSender cm = revertedSenderFactory.create(change);
- cm.setFrom(user.getAccountId());
- cm.setChangeMessage(cmsg);
- cm.send();
+ try {
+ final RevertedSender cm = revertedSenderFactory.create(change);
+ cm.setFrom(user.getAccountId());
+ cm.setChangeMessage(cmsg);
+ cm.send();
+ } catch (Exception err) {
+ log.error("Cannot send email for revert change " + change.getId(),
+ err);
+ }
return change.getId();
} finally {
@@ -315,13 +316,11 @@
}
public static Change.Id editCommitMessage(final PatchSet.Id patchSetId,
- final RefControl refControl, CommitValidators commitValidators,
- final IdentifiedUser user, final String message, final ReviewDb db,
+ final RefControl refControl, final IdentifiedUser user,
+ final String message, final ReviewDb db,
final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory,
- final ChangeHooks hooks, Repository git,
- final PatchSetInfoFactory patchSetInfoFactory,
- final GitReferenceUpdated gitRefUpdated, PersonIdent myIdent,
- final TrackingFooters trackingFooters)
+ Repository git, PersonIdent myIdent,
+ PatchSetInserter.Factory patchSetInserterFactory)
throws NoSuchChangeException, EmailException, OrmException,
MissingObjectException, IncorrectObjectTypeException, IOException,
InvalidChangeOperationException, PatchSetInfoNotAvailableException {
@@ -332,15 +331,18 @@
}
if (message == null || message.length() == 0) {
- throw new InvalidChangeOperationException("The commit message cannot be empty");
+ throw new InvalidChangeOperationException(
+ "The commit message cannot be empty");
}
final RevWalk revWalk = new RevWalk(git);
try {
RevCommit commit =
- revWalk.parseCommit(ObjectId.fromString(originalPS.getRevision().get()));
+ revWalk.parseCommit(ObjectId.fromString(originalPS.getRevision()
+ .get()));
if (commit.getFullMessage().equals(message)) {
- throw new InvalidChangeOperationException("New commit message cannot be same as existing commit message");
+ throw new InvalidChangeOperationException(
+ "New commit message cannot be same as existing commit message");
}
Date now = myIdent.getWhen();
@@ -372,97 +374,17 @@
newPatchSet.setRevision(new RevId(newCommit.name()));
newPatchSet.setDraft(originalPS.isDraft());
- final PatchSetInfo info =
- patchSetInfoFactory.get(newCommit, newPatchSet.getId());
+ final String msg =
+ "Patch Set " + newPatchSet.getPatchSetId()
+ + ": Commit message was updated";
- final String refName = newPatchSet.getRefName();
- CommitReceivedEvent commitReceivedEvent =
- new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
- newCommit.getId(), refName.substring(0,
- refName.lastIndexOf("/") + 1) + "new"), refControl
- .getProjectControl().getProject(), refControl.getRefName(),
- newCommit, user);
-
- try {
- commitValidators.validateForReceiveCommits(commitReceivedEvent);
- } catch (CommitValidationException e) {
- throw new InvalidChangeOperationException(e.getMessage());
- }
-
- final RefUpdate ru = git.updateRef(newPatchSet.getRefName());
- ru.setExpectedOldObjectId(ObjectId.zeroId());
- ru.setNewObjectId(newCommit);
- ru.disableRefLog();
- if (ru.update(revWalk) != RefUpdate.Result.NEW) {
- throw new IOException(String.format(
- "Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
- change.getDest().getParentKey().get(), ru.getResult()));
- }
- gitRefUpdated.fire(change.getProject(), ru);
-
- db.changes().beginTransaction(change.getId());
- try {
- Change updatedChange = db.changes().get(change.getId());
- if (updatedChange != null && updatedChange.getStatus().isOpen()) {
- change = updatedChange;
- } else {
- throw new InvalidChangeOperationException(String.format(
- "Change %s is closed", change.getId()));
- }
-
- ChangeUtil.insertAncestors(db, newPatchSet.getId(), commit);
- db.patchSets().insert(Collections.singleton(newPatchSet));
- updatedChange =
- db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- if (change.getStatus().isClosed()) {
- return null;
- }
- if (!change.currentPatchSetId().equals(patchSetId)) {
- return null;
- }
- if (change.getStatus() != Change.Status.DRAFT) {
- change.setStatus(Change.Status.NEW);
- }
- change.setLastSha1MergeTested(null);
- change.setCurrentPatchSet(info);
- ChangeUtil.updated(change);
- return change;
- }
- });
- if (updatedChange != null) {
- change = updatedChange;
- } else {
- throw new InvalidChangeOperationException(String.format(
- "Change %s was modified", change.getId()));
- }
-
- ApprovalsUtil.copyLabels(db,
- refControl.getProjectControl().getLabelTypes(),
- originalPS.getId(),
- change.currentPatchSetId());
-
- final List<FooterLine> footerLines = newCommit.getFooterLines();
- updateTrackingIds(db, change, trackingFooters, footerLines);
-
- final ChangeMessage cmsg =
- new ChangeMessage(new ChangeMessage.Key(changeId,
- ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
- final String msg = "Patch Set " + newPatchSet.getPatchSetId() + ": Commit message was updated";
- cmsg.setMessage(msg);
- db.changeMessages().insert(Collections.singleton(cmsg));
- db.commit();
-
- final CommitMessageEditedSender cm = commitMessageEditedSenderFactory.create(change);
- cm.setFrom(user.getAccountId());
- cm.setChangeMessage(cmsg);
- cm.send();
- } finally {
- db.rollback();
- }
-
- hooks.doPatchsetCreatedHook(change, newPatchSet, db);
+ change = patchSetInserterFactory
+ .create(git, revWalk, refControl, user, change, newCommit)
+ .setPatchSet(newPatchSet)
+ .setMessage(msg)
+ .setCopyLabels(true)
+ .setValidatePolicy(RECEIVE_COMMITS)
+ .insert();
return change.getId();
} finally {
@@ -475,6 +397,13 @@
final GitReferenceUpdated gitRefUpdated, final ReviewDb db)
throws NoSuchChangeException, OrmException, IOException {
final Change.Id changeId = patchSetId.getParentKey();
+ deleteDraftChange(changeId, gitManager, gitRefUpdated, db);
+ }
+
+ public static void deleteDraftChange(final Change.Id changeId,
+ GitRepositoryManager gitManager,
+ final GitReferenceUpdated gitRefUpdated, final ReviewDb db)
+ throws NoSuchChangeException, OrmException, IOException {
final Change change = db.changes().get(changeId);
if (change == null || change.getStatus() != Change.Status.DRAFT) {
throw new NoSuchChangeException(changeId);
@@ -496,7 +425,7 @@
final GitReferenceUpdated gitRefUpdated, final ReviewDb db)
throws NoSuchChangeException, OrmException, IOException {
final PatchSet.Id patchSetId = patch.getId();
- if (patch == null || !patch.isDraft()) {
+ if (!patch.isDraft()) {
throw new NoSuchChangeException(patchSetId.getParentKey());
}
@@ -534,7 +463,7 @@
// The encoding uses minutes since Wed Oct 1 00:00:00 2008 UTC.
// We overrun approximately 4,085 years later, so ~6093.
//
- final long lastUpdatedOn = (lastUpdated / 1000L) - 1222819200L;
+ final long lastUpdatedOn = (lastUpdated / 1000L) - SORT_KEY_EPOCH;
final StringBuilder r = new StringBuilder(16);
r.setLength(16);
formatHexInt(r, 0, (int) (lastUpdatedOn / 60));
@@ -542,6 +471,17 @@
return r.toString();
}
+ public static long parseSortKey(String sortKey) {
+ if ("z".equals(sortKey)) {
+ return Long.MAX_VALUE;
+ }
+ String ts = sortKey.substring(0, 8);
+ int i = 0;
+ while (i < 8 && ts.charAt(i) == '0')
+ i++;
+ return Long.parseLong(ts.substring(i), 16);
+ }
+
public static void computeSortKey(final Change c) {
long lastUpdated = c.getLastUpdatedOn().getTime();
int id = c.getId().get();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
index e64533c..0e86d34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
@@ -18,8 +18,6 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.config.FactoryModule;
-import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.args4j.AccountGroupIdHandler;
import com.google.gerrit.server.args4j.AccountGroupUUIDHandler;
import com.google.gerrit.server.args4j.AccountIdHandler;
@@ -28,11 +26,12 @@
import com.google.gerrit.server.args4j.PatchSetIdHandler;
import com.google.gerrit.server.args4j.ProjectControlHandler;
import com.google.gerrit.server.args4j.SocketAddressHandler;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.util.cli.CmdLineParser;
import com.google.gerrit.util.cli.OptionHandlerUtil;
-
+import com.google.gerrit.util.cli.OptionHandlers;
import org.eclipse.jgit.lib.ObjectId;
-
import org.kohsuke.args4j.spi.OptionHandler;
import java.net.SocketAddress;
@@ -44,6 +43,7 @@
@Override
protected void configure() {
factory(CmdLineParser.Factory.class);
+ bind(OptionHandlers.class);
registerOptionHandler(Account.Id.class, AccountIdHandler.class);
registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index 86a6ef8..4f2c6b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -51,6 +51,20 @@
}
/**
+ * Identity of the authenticated user.
+ * <p>
+ * In the normal case where a user authenticates as themselves
+ * {@code getRealUser() == this}.
+ * <p>
+ * If {@code X-Gerrit-RunAs} or {@code suexec} was used this method returns
+ * the identity of the account that has permission to act on behalf of this
+ * user.
+ */
+ public CurrentUser getRealUser() {
+ return this;
+ }
+
+ /**
* Get the set of groups the user is currently a member of.
* <p>
* The returned set may be a subset of the user's actual groups; if the user's
@@ -76,11 +90,14 @@
/** Capabilities available to this user account. */
public CapabilityControl getCapabilities() {
- CapabilityControl ctl = capabilities;
- if (ctl == null) {
- ctl = capabilityControlFactory.create(this);
- capabilities = ctl;
+ if (capabilities == null) {
+ capabilities = capabilityControlFactory.create(this);
}
- return ctl;
+ return capabilities;
+ }
+
+ /** Check if user is the IdentifiedUser */
+ public boolean isIdentifiedUser() {
+ return false;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index 3826293..7ab2564 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
import com.google.gerrit.common.data.AccountInfo;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
@@ -34,6 +35,7 @@
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
@@ -53,7 +55,6 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
-import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
@@ -97,13 +98,20 @@
public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
return new IdentifiedUser(capabilityControlFactory,
authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
- groupBackend, null, db, id);
+ groupBackend, null, db, id, null);
}
public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
return new IdentifiedUser(capabilityControlFactory,
authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
- groupBackend, Providers.of(remotePeer), null, id);
+ groupBackend, Providers.of(remotePeer), null, id, null);
+ }
+
+ public CurrentUser runAs(SocketAddress remotePeer, Account.Id id,
+ @Nullable CurrentUser caller) {
+ return new IdentifiedUser(capabilityControlFactory,
+ authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
+ groupBackend, Providers.of(remotePeer), null, id, caller);
}
}
@@ -152,7 +160,13 @@
public IdentifiedUser create(Account.Id id) {
return new IdentifiedUser(capabilityControlFactory,
authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
- groupBackend, remotePeerProvider, dbProvider, id);
+ groupBackend, remotePeerProvider, dbProvider, id, null);
+ }
+
+ public IdentifiedUser runAs(Account.Id id, CurrentUser caller) {
+ return new IdentifiedUser(capabilityControlFactory,
+ authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
+ groupBackend, remotePeerProvider, dbProvider, id, caller);
}
}
@@ -182,7 +196,9 @@
private Set<String> emailAddresses;
private GroupMembership effectiveGroups;
private Set<Change.Id> starredChanges;
+ private ResultSet<StarredChange> starredQuery;
private Collection<AccountProjectWatch> notificationFilters;
+ private CurrentUser realUser;
private IdentifiedUser(
CapabilityControl.Factory capabilityControlFactory,
@@ -192,7 +208,9 @@
final Realm realm, final AccountCache accountCache,
final GroupBackend groupBackend,
@Nullable final Provider<SocketAddress> remotePeerProvider,
- @Nullable final Provider<ReviewDb> dbProvider, final Account.Id id) {
+ @Nullable final Provider<ReviewDb> dbProvider,
+ final Account.Id id,
+ @Nullable CurrentUser realUser) {
super(capabilityControlFactory);
this.canonicalUrl = canonicalUrl;
this.accountCache = accountCache;
@@ -202,6 +220,12 @@
this.remotePeerProvider = remotePeerProvider;
this.dbProvider = dbProvider;
this.accountId = id;
+ this.realUser = realUser != null ? realUser : this;
+ }
+
+ @Override
+ public CurrentUser getRealUser() {
+ return realUser;
}
// TODO(cranger): maybe get the state through the accountCache instead.
@@ -274,11 +298,18 @@
if (dbProvider == null) {
throw new OutOfScopeException("Not in request scoped user");
}
- final Set<Change.Id> h = new HashSet<Change.Id>();
+ Set<Change.Id> h = Sets.newHashSet();
try {
- for (final StarredChange sc : dbProvider.get().starredChanges()
- .byAccount(getAccountId())) {
- h.add(sc.getChangeId());
+ if (starredQuery != null) {
+ for (StarredChange sc : starredQuery) {
+ h.add(sc.getChangeId());
+ }
+ starredQuery = null;
+ } else {
+ for (StarredChange sc : dbProvider.get().starredChanges()
+ .byAccount(getAccountId())) {
+ h.add(sc.getChangeId());
+ }
}
} catch (OrmException e) {
log.warn("Cannot query starred by user changes", e);
@@ -288,6 +319,29 @@
return starredChanges;
}
+ public void asyncStarredChanges() {
+ if (starredChanges == null && dbProvider != null) {
+ try {
+ starredQuery =
+ dbProvider.get().starredChanges().byAccount(getAccountId());
+ } catch (OrmException e) {
+ log.warn("Cannot query starred by user changes", e);
+ starredQuery = null;
+ starredChanges = Collections.emptySet();
+ }
+ }
+ }
+
+ public void abortStarredChanges() {
+ if (starredQuery != null) {
+ try {
+ starredQuery.close();
+ } finally {
+ starredQuery = null;
+ }
+ }
+ }
+
@Override
public Collection<AccountProjectWatch> getNotificationFilters() {
if (notificationFilters == null) {
@@ -390,4 +444,10 @@
public String toString() {
return "IdentifiedUser[account " + getAccountId() + "]";
}
+
+ /** Check if user is the IdentifiedUser */
+ @Override
+ public boolean isIdentifiedUser() {
+ return true;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessCollection.java
new file mode 100644
index 0000000..58f93d8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessCollection.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.access;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class AccessCollection implements
+ RestCollection<TopLevelResource, AccessResource> {
+ private final Provider<ListAccess> list;
+ private final DynamicMap<RestView<AccessResource>> views;
+
+ @Inject
+ AccessCollection(Provider<ListAccess> list,
+ DynamicMap<RestView<AccessResource>> views) {
+ this.list = list;
+ this.views = views;
+ }
+
+ @Override
+ public RestView<TopLevelResource> list() {
+ return list.get();
+ }
+
+ @Override
+ public AccessResource parse(TopLevelResource parent, IdString id)
+ throws ResourceNotFoundException {
+ throw new ResourceNotFoundException(id);
+ }
+
+ @Override
+ public DynamicMap<RestView<AccessResource>> views() {
+ return views;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessResource.java
new file mode 100644
index 0000000..22888b8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/AccessResource.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.access;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class AccessResource implements RestResource {
+ public static final TypeLiteral<RestView<AccessResource>> ACCESS_KIND =
+ new TypeLiteral<RestView<AccessResource>>() {};
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
new file mode 100644
index 0000000..4002c1a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
@@ -0,0 +1,308 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.access;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.GroupJson;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectJson;
+import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.RefControl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ListAccess implements RestReadView<TopLevelResource> {
+
+ @Option(name = "--project", aliases = {"-p"}, metaVar = "PROJECT",
+ usage = "projects for which the access rights should be returned")
+ private List<String> projects = Lists.newArrayList();
+
+ private final Provider<CurrentUser> self;
+ private final ProjectControl.GenericFactory projectControlFactory;
+ private final ProjectCache projectCache;
+ private final ProjectJson projectJson;
+ private final MetaDataUpdate.Server metaDataUpdateFactory;
+ private final GroupControl.Factory groupControlFactory;
+ private final GroupBackend groupBackend;
+ private final AllProjectsName allProjectsName;
+
+ @Inject
+ public ListAccess(Provider<CurrentUser> self,
+ ProjectControl.GenericFactory projectControlFactory,
+ ProjectCache projectCache, ProjectJson projectJson,
+ MetaDataUpdate.Server metaDataUpdateFactory,
+ GroupControl.Factory groupControlFactory, GroupBackend groupBackend,
+ GroupJson groupJson, AllProjectsName allProjectsName) {
+ this.self = self;
+ this.projectControlFactory = projectControlFactory;
+ this.projectCache = projectCache;
+ this.projectJson = projectJson;
+ this.metaDataUpdateFactory = metaDataUpdateFactory;
+ this.groupControlFactory = groupControlFactory;
+ this.groupBackend = groupBackend;
+ this.allProjectsName = allProjectsName;
+ }
+
+ @Override
+ public Map<String, ProjectAccessInfo> apply(TopLevelResource resource)
+ throws ResourceNotFoundException, ResourceConflictException, IOException {
+ Map<String, ProjectAccessInfo> access = Maps.newTreeMap();
+ for (String p: projects) {
+ Project.NameKey projectName = new Project.NameKey(p);
+ ProjectControl pc = open(projectName);
+ ProjectConfig config;
+
+ try {
+ // Load the current configuration from the repository, ensuring it's the most
+ // recent version available. If it differs from what was in the project
+ // state, force a cache flush now.
+ //
+ MetaDataUpdate md = metaDataUpdateFactory.create(projectName);
+ try {
+ config = ProjectConfig.read(md);
+
+ if (config.updateGroupNames(groupBackend)) {
+ md.setMessage("Update group names\n");
+ config.commit(md);
+ projectCache.evict(config.getProject());
+ pc = open(projectName);
+ } else if (config.getRevision() != null
+ && !config.getRevision().equals(
+ pc.getProjectState().getConfig().getRevision())) {
+ projectCache.evict(config.getProject());
+ pc = open(projectName);
+ }
+ } catch (ConfigInvalidException e) {
+ throw new ResourceConflictException(e.getMessage());
+ } finally {
+ md.close();
+ }
+ } catch (RepositoryNotFoundException e) {
+ throw new ResourceNotFoundException(p);
+ }
+
+ access.put(p, new ProjectAccessInfo(pc, config));
+ }
+ return access;
+ }
+
+ private ProjectControl open(Project.NameKey projectName)
+ throws ResourceNotFoundException, IOException {
+ try {
+ return projectControlFactory.validateFor(projectName,
+ ProjectControl.OWNER | ProjectControl.VISIBLE, self.get());
+ } catch (NoSuchProjectException e) {
+ throw new ResourceNotFoundException(projectName.get());
+ }
+ }
+
+ public class ProjectAccessInfo {
+ public String revision;
+ public ProjectInfo inheritsFrom;
+ public Map<String, AccessSectionInfo> local;
+ public Boolean isOwner;
+ public Set<String> ownerOf;
+ public Boolean canUpload;
+ public Boolean canAdd;
+ public Boolean configVisible;
+
+ public ProjectAccessInfo(ProjectControl pc, ProjectConfig config) {
+ final RefControl metaConfigControl =
+ pc.controlForRef(GitRepositoryManager.REF_CONFIG);
+ local = Maps.newHashMap();
+ ownerOf = Sets.newHashSet();
+ Map<AccountGroup.UUID, Boolean> visibleGroups =
+ new HashMap<AccountGroup.UUID, Boolean>();
+
+ for (AccessSection section : config.getAccessSections()) {
+ String name = section.getName();
+ if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
+ if (pc.isOwner()) {
+ local.put(name, new AccessSectionInfo(section));
+ ownerOf.add(name);
+
+ } else if (metaConfigControl.isVisible()) {
+ local.put(section.getName(), new AccessSectionInfo(section));
+ }
+
+ } else if (RefConfigSection.isValid(name)) {
+ RefControl rc = pc.controlForRef(name);
+ if (rc.isOwner()) {
+ local.put(name, new AccessSectionInfo(section));
+ ownerOf.add(name);
+
+ } else if (metaConfigControl.isVisible()) {
+ local.put(name, new AccessSectionInfo(section));
+
+ } else if (rc.isVisible()) {
+ // Filter the section to only add rules describing groups that
+ // are visible to the current-user. This includes any group the
+ // user is a member of, as well as groups they own or that
+ // are visible to all users.
+
+ AccessSection dst = null;
+ for (Permission srcPerm : section.getPermissions()) {
+ Permission dstPerm = null;
+
+ for (PermissionRule srcRule : srcPerm.getRules()) {
+ AccountGroup.UUID group = srcRule.getGroup().getUUID();
+ if (group == null) {
+ continue;
+ }
+
+ Boolean canSeeGroup = visibleGroups.get(group);
+ if (canSeeGroup == null) {
+ try {
+ canSeeGroup = groupControlFactory.controlFor(group).isVisible();
+ } catch (NoSuchGroupException e) {
+ canSeeGroup = Boolean.FALSE;
+ }
+ visibleGroups.put(group, canSeeGroup);
+ }
+
+ if (canSeeGroup) {
+ if (dstPerm == null) {
+ if (dst == null) {
+ dst = new AccessSection(name);
+ local.put(name, new AccessSectionInfo(dst));
+ }
+ dstPerm = dst.getPermission(srcPerm.getName(), true);
+ }
+ dstPerm.add(srcRule);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (ownerOf.isEmpty() && pc.isOwnerAnyRef()) {
+ // Special case: If the section list is empty, this project has no current
+ // access control information. Rely on what ProjectControl determines
+ // is ownership, which probably means falling back to site administrators.
+ ownerOf.add(AccessSection.ALL);
+ }
+
+
+ if (config.getRevision() != null) {
+ revision = config.getRevision().name();
+ }
+
+ ProjectState parent =
+ Iterables.getFirst(pc.getProjectState().parents(), null);
+ if (parent != null) {
+ inheritsFrom = projectJson.format(parent.getProject());
+ }
+
+ if (pc.getProject().getNameKey().equals(allProjectsName)) {
+ if (pc.isOwner()) {
+ ownerOf.add(AccessSection.GLOBAL_CAPABILITIES);
+ }
+ }
+
+ isOwner = toBoolean(pc.isOwner());
+ canUpload = toBoolean(pc.isOwner()
+ || (metaConfigControl.isVisible() && metaConfigControl.canUpload()));
+ canAdd = toBoolean(pc.canAddRefs());
+ configVisible = pc.isOwner() || metaConfigControl.isVisible();
+ }
+ }
+
+ public class AccessSectionInfo {
+ public Map<String, PermissionInfo> permissions;
+
+ public AccessSectionInfo(AccessSection section) {
+ permissions = Maps.newHashMap();
+ for (Permission p : section.getPermissions()) {
+ permissions.put(p.getName(), new PermissionInfo(p));
+ }
+ }
+ }
+
+ public class PermissionInfo {
+ public String label;
+ public Boolean exclusive;
+ public Map<String, PermissionRuleInfo> rules;
+
+ public PermissionInfo(Permission permission) {
+ label = permission.getLabel();
+ exclusive = toBoolean(permission.getExclusiveGroup());
+ rules = Maps.newHashMap();
+ for (PermissionRule r : permission.getRules()) {
+ rules.put(r.getGroup().getUUID().get(), new PermissionRuleInfo(r));
+ }
+ }
+ }
+
+ public class PermissionRuleInfo {
+ public PermissionRule.Action action;
+ public Boolean force;
+ public Integer min;
+ public Integer max;
+
+
+ public PermissionRuleInfo(PermissionRule rule) {
+ action = rule.getAction();
+ force = toBoolean(rule.getForce());
+ if (hasRange(rule)) {
+ min = rule.getMin();
+ max = rule.getMax();
+ }
+ }
+
+ private boolean hasRange(PermissionRule rule) {
+ return (!(rule.getMin() == null || rule.getMin() == 0))
+ || (!(rule.getMax() == null || rule.getMax() == 0));
+ }
+ }
+
+ private static Boolean toBoolean(boolean value) {
+ return value ? true : null;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/Module.java
new file mode 100644
index 0000000..cd0d334
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/Module.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.access;
+
+import static com.google.gerrit.server.access.AccessResource.ACCESS_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+
+public class Module extends RestApiModule {
+ @Override
+ protected void configure() {
+ bind(AccessCollection.class);
+
+ DynamicMap.mapOf(binder(), ACCESS_KIND);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
index f148b31..1440eac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
@@ -96,7 +96,7 @@
*/
public boolean canSee(final Account.Id otherUser) {
// Special case: I can always see myself.
- if (currentUser instanceof IdentifiedUser
+ if (currentUser.isIdentifiedUser()
&& ((IdentifiedUser) currentUser).getAccountId().equals(otherUser)) {
return true;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
new file mode 100644
index 0000000..a4881a4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import java.util.Set;
+
+/**
+ * Directory of user account information.
+ *
+ * Implementations supply data to Gerrit about user accounts.
+ */
+public abstract class AccountDirectory {
+ /** Fields to be populated for a REST API response. */
+ public enum FillOptions {
+ /** Human friendly display name presented in the web interface. */
+ NAME,
+
+ /** Preferred email address to contact the user at. */
+ EMAIL,
+
+ /** User profile images. */
+ AVATARS,
+
+ /** Unique user identity to login to Gerrit, may be deprecated. */
+ USERNAME;
+ }
+
+ public abstract void fillAccountInfo(
+ Iterable<? extends AccountInfo> in,
+ Set<FillOptions> options)
+ throws DirectoryException;
+
+ @SuppressWarnings("serial")
+ public static class DirectoryException extends Exception {
+ public DirectoryException(String message) {
+ super(message);
+ }
+
+ public DirectoryException(String message, Throwable why) {
+ super(message, why);
+ }
+
+ public DirectoryException(Throwable why) {
+ super(why);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java
index a296716..cde59e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java
@@ -14,40 +14,44 @@
package com.google.gerrit.server.account;
-import com.google.common.collect.ArrayListMultimap;
+import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountDirectory.DirectoryException;
+import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
public class AccountInfo {
public static class Loader {
+ private static final Set<FillOptions> DETAILED_OPTIONS =
+ Collections.unmodifiableSet(EnumSet.of(
+ FillOptions.NAME,
+ FillOptions.EMAIL,
+ FillOptions.AVATARS));
+
public interface Factory {
Loader create(boolean detailed);
}
- private final Provider<ReviewDb> db;
- private final AccountCache accountCache;
+ private final InternalAccountDirectory directory;
private final boolean detailed;
private final Map<Account.Id, AccountInfo> created;
private final List<AccountInfo> provided;
@Inject
- Loader(Provider<ReviewDb> db,
- AccountCache accountCache,
- @Assisted boolean detailed) {
- this.db = db;
- this.accountCache = accountCache;
+ Loader(InternalAccountDirectory directory, @Assisted boolean detailed) {
+ this.directory = directory;
this.detailed = detailed;
created = Maps.newHashMap();
provided = Lists.newArrayList();
@@ -60,31 +64,29 @@
AccountInfo info = created.get(id);
if (info == null) {
info = new AccountInfo(id);
+ if (detailed) {
+ info._account_id = id.get();
+ }
created.put(id, info);
}
return info;
}
public void put(AccountInfo info) {
+ if (detailed) {
+ info._account_id = info._id.get();
+ }
provided.add(info);
}
public void fill() throws OrmException {
- Multimap<Account.Id, AccountInfo> missing = ArrayListMultimap.create();
- for (AccountInfo info : Iterables.concat(created.values(), provided)) {
- AccountState state = accountCache.getIfPresent(info._id);
- if (state != null) {
- info.fill(state.getAccount(), detailed);
- } else {
- missing.put(info._id, info);
- }
- }
- if (!missing.isEmpty()) {
- for (Account account : db.get().accounts().get(missing.keySet())) {
- for (AccountInfo info : missing.get(account.getId())) {
- info.fill(account, detailed);
- }
- }
+ try {
+ directory.fillAccountInfo(
+ Iterables.concat(created.values(), provided),
+ DETAILED_OPTIONS);
+ } catch (DirectoryException e) {
+ Throwables.propagateIfPossible(e.getCause(), OrmException.class);
+ throw new OrmException(e);
}
}
@@ -97,12 +99,6 @@
}
}
- public static AccountInfo parse(Account a, boolean detailed) {
- AccountInfo ai = new AccountInfo(a.getId());
- ai.fill(a, detailed);
- return ai;
- }
-
public transient Account.Id _id;
public AccountInfo(Account.Id id) {
@@ -112,12 +108,22 @@
public Integer _account_id;
public String name;
public String email;
+ public String username;
+ public List<AvatarInfo> avatars;
- private void fill(Account account, boolean detailed) {
- name = account.getFullName();
- if (detailed) {
- _account_id = account.getId().get();
- email = account.getPreferredEmail();
- }
+ public static class AvatarInfo {
+ /**
+ * Size in pixels the UI prefers an avatar image to be.
+ *
+ * The web UI prefers avatar images to be square, both
+ * the height and width of the image should be this size.
+ * The height is the more important dimension to match
+ * than the width.
+ */
+ public static final int DEFAULT_SIZE = 26;
+
+ public String url;
+ public Integer height;
+ public Integer width;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 1827446..10b34ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -386,46 +386,42 @@
* cannot be linked at this time.
*/
public AuthResult link(final Account.Id to, AuthRequest who)
- throws AccountException {
+ throws AccountException, OrmException {
+ final ReviewDb db = schema.open();
try {
- final ReviewDb db = schema.open();
- try {
- who = realm.link(db, to, who);
+ who = realm.link(db, to, who);
- final AccountExternalId.Key key = id(who);
- AccountExternalId extId = db.accountExternalIds().get(key);
- if (extId != null) {
- if (!extId.getAccountId().equals(to)) {
- throw new AccountException("Identity in use by another account");
- }
- update(db, who, extId);
+ final AccountExternalId.Key key = id(who);
+ AccountExternalId extId = db.accountExternalIds().get(key);
+ if (extId != null) {
+ if (!extId.getAccountId().equals(to)) {
+ throw new AccountException("Identity in use by another account");
+ }
+ update(db, who, extId);
- } else {
- extId = createId(to, who);
- extId.setEmailAddress(who.getEmailAddress());
- db.accountExternalIds().insert(Collections.singleton(extId));
+ } else {
+ extId = createId(to, who);
+ extId.setEmailAddress(who.getEmailAddress());
+ db.accountExternalIds().insert(Collections.singleton(extId));
- if (who.getEmailAddress() != null) {
- final Account a = db.accounts().get(to);
- if (a.getPreferredEmail() == null) {
- a.setPreferredEmail(who.getEmailAddress());
- db.accounts().update(Collections.singleton(a));
- }
- }
-
- if (who.getEmailAddress() != null) {
- byEmailCache.evict(who.getEmailAddress());
- byIdCache.evict(to);
+ if (who.getEmailAddress() != null) {
+ final Account a = db.accounts().get(to);
+ if (a.getPreferredEmail() == null) {
+ a.setPreferredEmail(who.getEmailAddress());
+ db.accounts().update(Collections.singleton(a));
}
}
- return new AuthResult(to, key, false);
-
- } finally {
- db.close();
+ if (who.getEmailAddress() != null) {
+ byEmailCache.evict(who.getEmailAddress());
+ byIdCache.evict(to);
+ }
}
- } catch (OrmException e) {
- throw new AccountException("Cannot link identity", e);
+
+ return new AuthResult(to, key, false);
+
+ } finally {
+ db.close();
}
}
@@ -439,42 +435,38 @@
* cannot be unlinked at this time.
*/
public AuthResult unlink(final Account.Id from, AuthRequest who)
- throws AccountException {
+ throws AccountException, OrmException {
+ final ReviewDb db = schema.open();
try {
- final ReviewDb db = schema.open();
- try {
- who = realm.unlink(db, from, who);
+ who = realm.unlink(db, from, who);
- final AccountExternalId.Key key = id(who);
- AccountExternalId extId = db.accountExternalIds().get(key);
- if (extId != null) {
- if (!extId.getAccountId().equals(from)) {
- throw new AccountException("Identity in use by another account");
+ final AccountExternalId.Key key = id(who);
+ AccountExternalId extId = db.accountExternalIds().get(key);
+ if (extId != null) {
+ if (!extId.getAccountId().equals(from)) {
+ throw new AccountException("Identity in use by another account");
+ }
+ db.accountExternalIds().delete(Collections.singleton(extId));
+
+ if (who.getEmailAddress() != null) {
+ final Account a = db.accounts().get(from);
+ if (a.getPreferredEmail() != null
+ && a.getPreferredEmail().equals(who.getEmailAddress())) {
+ a.setPreferredEmail(null);
+ db.accounts().update(Collections.singleton(a));
}
- db.accountExternalIds().delete(Collections.singleton(extId));
-
- if (who.getEmailAddress() != null) {
- final Account a = db.accounts().get(from);
- if (a.getPreferredEmail() != null
- && a.getPreferredEmail().equals(who.getEmailAddress())) {
- a.setPreferredEmail(null);
- db.accounts().update(Collections.singleton(a));
- }
- byEmailCache.evict(who.getEmailAddress());
- byIdCache.evict(from);
- }
-
- } else {
- throw new AccountException("Identity not found");
+ byEmailCache.evict(who.getEmailAddress());
+ byIdCache.evict(from);
}
- return new AuthResult(from, key, false);
-
- } finally {
- db.close();
+ } else {
+ throw new AccountException("Identity not found");
}
- } catch (OrmException e) {
- throw new AccountException("Cannot unlink identity", e);
+
+ return new AuthResult(from, key, false);
+
+ } finally {
+ db.close();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 67d87b3..383ed05 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -56,7 +56,22 @@
*/
public Account find(final String nameOrEmail) throws OrmException {
Set<Account.Id> r = findAll(nameOrEmail);
- return r.size() == 1 ? byId.get(r.iterator().next()).getAccount() : null;
+ if (r.size() == 1) {
+ return byId.get(r.iterator().next()).getAccount();
+ }
+
+ Account match = null;
+ for (Account.Id id : r) {
+ Account account = byId.get(id).getAccount();
+ if (!account.isActive()) {
+ continue;
+ }
+ if (match != null) {
+ return null;
+ }
+ match = account;
+ }
+ return match;
}
/**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
index 9dc423a..629bd15 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
@@ -16,6 +16,7 @@
import com.google.gerrit.extensions.restapi.RestResource;
import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.server.IdentifiedUser;
import com.google.inject.TypeLiteral;
@@ -26,6 +27,12 @@
public static final TypeLiteral<RestView<Capability>> CAPABILITY_KIND =
new TypeLiteral<RestView<Capability>>() {};
+ public static final TypeLiteral<RestView<Email>> EMAIL_KIND =
+ new TypeLiteral<RestView<Email>>() {};
+
+ public static final TypeLiteral<RestView<SshKey>> SSH_KEY_KIND =
+ new TypeLiteral<RestView<SshKey>>() {};
+
private final IdentifiedUser user;
public AccountResource(IdentifiedUser user) {
@@ -57,4 +64,30 @@
return user.getCapabilities().canPerform(getCapability());
}
}
+
+ public static class Email extends AccountResource {
+ private final String email;
+
+ public Email(IdentifiedUser user, String email) {
+ super(user);
+ this.email = email;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+ }
+
+ public static class SshKey extends AccountResource {
+ private final AccountSshKey sshKey;
+
+ public SshKey(IdentifiedUser user, AccountSshKey sshKey) {
+ super(user);
+ this.sshKey = sshKey;
+ }
+
+ public AccountSshKey getSshKey() {
+ return sshKey;
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index 674046c..7d3c06e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.account;
-import com.google.common.collect.Iterables;
import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -31,27 +31,29 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
-import java.util.Set;
-
public class AccountsCollection implements
- RestCollection<TopLevelResource, AccountResource> {
+ RestCollection<TopLevelResource, AccountResource>,
+ AcceptsCreate<TopLevelResource>{
private final Provider<CurrentUser> self;
private final AccountResolver resolver;
private final AccountControl.Factory accountControlFactory;
private final IdentifiedUser.GenericFactory userFactory;
private final DynamicMap<RestView<AccountResource>> views;
+ private final CreateAccount.Factory createAccountFactory;
@Inject
AccountsCollection(Provider<CurrentUser> self,
AccountResolver resolver,
AccountControl.Factory accountControlFactory,
IdentifiedUser.GenericFactory userFactory,
- DynamicMap<RestView<AccountResource>> views) {
+ DynamicMap<RestView<AccountResource>> views,
+ CreateAccount.Factory createAccountFactory) {
this.self = self;
this.resolver = resolver;
this.accountControlFactory = accountControlFactory;
this.userFactory = userFactory;
this.views = views;
+ this.createAccountFactory = createAccountFactory;
}
@Override
@@ -73,7 +75,7 @@
* "Full Name <email@example.com>", just the email address, a full name
* if it is unique, an account ID, a user name or 'self' for the
* calling user
- * @return the project
+ * @return the user, never null.
* @throws UnprocessableEntityException thrown if the account ID cannot be
* resolved or if the account is not visible to the calling user
*/
@@ -91,7 +93,7 @@
CurrentUser user = self.get();
if (id.equals("self")) {
- if (user instanceof IdentifiedUser) {
+ if (user.isIdentifiedUser()) {
return (IdentifiedUser) user;
} else if (user instanceof AnonymousUser) {
throw new AuthException("Authentication required");
@@ -100,11 +102,11 @@
}
}
- Set<Account.Id> matches = resolver.findAll(id);
- if (matches.size() != 1) {
+ Account match = resolver.find(id);
+ if (match == null) {
return null;
}
- return userFactory.create(Iterables.getOnlyElement(matches));
+ return userFactory.create(match.getId());
}
@Override
@@ -116,4 +118,10 @@
public DynamicMap<RestView<AccountResource>> views() {
return views;
}
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public CreateAccount create(TopLevelResource parent, IdString username) {
+ return createAccountFactory.create(username.get());
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
new file mode 100644
index 0000000..2cff009
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.ByteSource;
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RawInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AddSshKey.Input;
+import com.google.gerrit.server.account.GetSshKeys.SshKeyInfo;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+
+public class AddSshKey implements RestModifyView<AccountResource, Input> {
+ public static class Input {
+ public RawInput raw;
+ }
+
+ private final Provider<CurrentUser> self;
+ private final Provider<ReviewDb> dbProvider;
+ private final SshKeyCache sshKeyCache;
+
+ @Inject
+ AddSshKey(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider,
+ SshKeyCache sshKeyCache) {
+ this.self = self;
+ this.dbProvider = dbProvider;
+ this.sshKeyCache = sshKeyCache;
+ }
+
+ @Override
+ public Response<SshKeyInfo> apply(AccountResource rsrc, Input input)
+ throws AuthException, MethodNotAllowedException, BadRequestException,
+ ResourceConflictException, OrmException, IOException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("not allowed to add SSH keys");
+ }
+ if (input == null) {
+ input = new Input();
+ }
+ if (input.raw == null) {
+ throw new BadRequestException("SSH public key missing");
+ }
+
+ int max = 0;
+ for (AccountSshKey k : dbProvider.get().accountSshKeys()
+ .byAccount(rsrc.getUser().getAccountId())) {
+ max = Math.max(max, k.getKey().get());
+ }
+
+ final RawInput rawKey = input.raw;
+ String sshPublicKey = new ByteSource() {
+ @Override
+ public InputStream openStream() throws IOException {
+ return rawKey.getInputStream();
+ }
+ }.asCharSource(Charsets.UTF_8).read();
+
+ try {
+ AccountSshKey sshKey =
+ sshKeyCache.create(new AccountSshKey.Id(
+ rsrc.getUser().getAccountId(), max + 1), sshPublicKey);
+ dbProvider.get().accountSshKeys().insert(Collections.singleton(sshKey));
+ sshKeyCache.evict(rsrc.getUser().getUserName());
+ return Response.<SshKeyInfo>created(new SshKeyInfo(sshKey));
+ } catch (InvalidSshKeyException e) {
+ throw new BadRequestException(e.getMessage());
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index d2014ec..cafc540 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -65,8 +65,12 @@
/** @return true if the user can administer this server. */
public boolean canAdministrateServer() {
if (canAdministrateServer == null) {
- canAdministrateServer = user instanceof PeerDaemonUser
- || matchAny(capabilities.administrateServer, ALLOWED_RULE);
+ if (user.getRealUser() != user) {
+ canAdministrateServer = false;
+ } else {
+ canAdministrateServer = user instanceof PeerDaemonUser
+ || matchAny(capabilities.administrateServer, ALLOWED_RULE);
+ }
}
return canAdministrateServer;
}
@@ -130,18 +134,11 @@
|| canAdministrateServer();
}
-
/** @return true if the user can access the database (with gsql). */
public boolean canAccessDatabase() {
return canPerform(GlobalCapability.ACCESS_DATABASE);
}
- /** @return true if the user can force replication to any configured destination. */
- public boolean canStartReplication() {
- return canPerform(GlobalCapability.START_REPLICATION)
- || canAdministrateServer();
- }
-
/** @return true if the user can stream Gerrit events. */
public boolean canStreamEvents() {
return canPerform(GlobalCapability.STREAM_EVENTS)
@@ -154,6 +151,17 @@
|| canAdministrateServer();
}
+ /** @return true if the user can generate HTTP passwords for users other than self. */
+ public boolean canGenerateHttpPassword() {
+ return canPerform(GlobalCapability.GENERATE_HTTP_PASSWORD)
+ || canAdministrateServer();
+ }
+
+ /** @return true if the user can impersonate another user. */
+ public boolean canRunAs() {
+ return canPerform(GlobalCapability.RUN_AS);
+ }
+
/** @return which priority queue the user's tasks should be submitted to. */
public QueueProvider.QueueType getQueueType() {
// If a non-generic group (that is not Anonymous Users or Registered Users)
@@ -216,9 +224,18 @@
List<PermissionRule> ruleList) {
int min = 0;
int max = 0;
- for (PermissionRule rule : ruleList) {
- min = Math.min(min, rule.getMin());
- max = Math.max(max, rule.getMax());
+ if (ruleList.isEmpty()) {
+ PermissionRange.WithDefaults defaultRange =
+ GlobalCapability.getRange(permissionName);
+ if (defaultRange != null) {
+ min = defaultRange.getDefaultMin();
+ max = defaultRange.getDefaultMax();
+ }
+ } else {
+ for (PermissionRule rule : ruleList) {
+ min = Math.min(min, rule.getMin());
+ max = Math.max(max, rule.getMax());
+ }
}
return new PermissionRange(permissionName, min, max);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
new file mode 100644
index 0000000..6b68032
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.annotations.CapabilityScope;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.inject.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.annotation.Annotation;
+
+public class CapabilityUtils {
+ private static final Logger log = LoggerFactory
+ .getLogger(CapabilityUtils.class);
+
+ public static void checkRequiresCapability(Provider<CurrentUser> userProvider,
+ String pluginName, Class<?> clazz)
+ throws AuthException {
+ RequiresCapability rc = getClassAnnotation(clazz, RequiresCapability.class);
+ if (rc != null) {
+ CurrentUser user = userProvider.get();
+ CapabilityControl ctl = user.getCapabilities();
+ if (ctl.canAdministrateServer()) {
+ return;
+ }
+
+ String capability = rc.value();
+ if (pluginName != null && !"gerrit".equals(pluginName)
+ && (rc.scope() == CapabilityScope.PLUGIN
+ || rc.scope() == CapabilityScope.CONTEXT)) {
+ capability = String.format("%s-%s", pluginName, rc.value());
+ } else if (rc.scope() == CapabilityScope.PLUGIN) {
+ log.error(String.format(
+ "Class %s uses @%s(scope=%s), but is not within a plugin",
+ clazz.getName(),
+ RequiresCapability.class.getSimpleName(),
+ CapabilityScope.PLUGIN.name()));
+ throw new AuthException("cannot check capability");
+ }
+
+ if (!ctl.canPerform(capability)) {
+ throw new AuthException(String.format(
+ "Capability %s is required to access this resource",
+ capability));
+ }
+ }
+ }
+
+ /**
+ * Find an instance of the specified annotation, walking up the inheritance
+ * tree if necessary.
+ *
+ * @param <T> Annotation type to search for
+ * @param clazz root class to search, may be null
+ * @param annotationClass class object of Annotation subclass to search for
+ * @return the requested annotation or null if none
+ */
+ private static <T extends Annotation> T getClassAnnotation(Class<?> clazz,
+ Class<T> annotationClass) {
+ for (; clazz != null; clazz = clazz.getSuperclass()) {
+ T t = clazz.getAnnotation(annotationClass);
+ if (t != null) {
+ return t;
+ }
+ }
+ return null;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java
deleted file mode 100644
index 255c248..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.account;
-
-import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import java.util.Collections;
-import java.util.concurrent.Callable;
-
-/** Operation to clear a password for an account. */
-public class ClearPassword implements Callable<AccountExternalId> {
- public interface Factory {
- ClearPassword create(AccountExternalId.Key forUser);
- }
-
- private final AccountCache accountCache;
- private final ReviewDb db;
- private final IdentifiedUser user;
-
- private final AccountExternalId.Key forUser;
-
- @Inject
- ClearPassword(final AccountCache accountCache, final ReviewDb db,
- final IdentifiedUser user,
-
- @Assisted AccountExternalId.Key forUser) {
- this.accountCache = accountCache;
- this.db = db;
- this.user = user;
-
- this.forUser = forUser;
- }
-
- public AccountExternalId call() throws OrmException, NoSuchEntityException {
- AccountExternalId id = db.accountExternalIds().get(forUser);
- if (id == null || !user.getAccountId().equals(id.getAccountId())) {
- throw new NoSuchEntityException();
- }
-
- id.setPassword(null);
- db.accountExternalIds().update(Collections.singleton(id));
- accountCache.evict(user.getAccountId());
- return id;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
new file mode 100644
index 0000000..4b61cec
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -0,0 +1,206 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.CreateAccount.Input;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+@RequiresCapability(GlobalCapability.CREATE_ACCOUNT)
+public class CreateAccount implements RestModifyView<TopLevelResource, Input> {
+ public static class Input {
+ @DefaultInput
+ public String username;
+ public String name;
+ public String email;
+ public String sshKey;
+ public String httpPassword;
+ public List<String> groups;
+ }
+
+ public static interface Factory {
+ CreateAccount create(String username);
+ }
+
+ private final ReviewDb db;
+ private final IdentifiedUser currentUser;
+ private final GroupsCollection groupsCollection;
+ private final SshKeyCache sshKeyCache;
+ private final AccountCache accountCache;
+ private final AccountByEmailCache byEmailCache;
+ private final AccountInfo.Loader.Factory infoLoader;
+ private final String username;
+
+ @Inject
+ CreateAccount(ReviewDb db, IdentifiedUser currentUser,
+ GroupsCollection groupsCollection, SshKeyCache sshKeyCache,
+ AccountCache accountCache, AccountByEmailCache byEmailCache,
+ AccountInfo.Loader.Factory infoLoader,
+ @Assisted String username) {
+ this.db = db;
+ this.currentUser = currentUser;
+ this.groupsCollection = groupsCollection;
+ this.sshKeyCache = sshKeyCache;
+ this.accountCache = accountCache;
+ this.byEmailCache = byEmailCache;
+ this.infoLoader = infoLoader;
+ this.username = username;
+ }
+
+ @Override
+ public Object apply(TopLevelResource rsrc, Input input)
+ throws BadRequestException, ResourceConflictException,
+ UnprocessableEntityException, OrmException {
+ if (input == null) {
+ input = new Input();
+ }
+ if (input.username != null && !username.equals(input.username)) {
+ throw new BadRequestException("username must match URL");
+ }
+
+ if (!username.matches(Account.USER_NAME_PATTERN)) {
+ throw new BadRequestException("Username '" + username + "'"
+ + " must contain only letters, numbers, _, - or .");
+ }
+
+ Set<AccountGroup.Id> groups = parseGroups(input.groups);
+
+ Account.Id id = new Account.Id(db.nextAccountId());
+ AccountSshKey key = createSshKey(id, input.sshKey);
+
+ AccountExternalId extUser =
+ new AccountExternalId(id, new AccountExternalId.Key(
+ AccountExternalId.SCHEME_USERNAME, username));
+
+ if (input.httpPassword != null) {
+ extUser.setPassword(input.httpPassword);
+ }
+
+ if (db.accountExternalIds().get(extUser.getKey()) != null) {
+ throw new ResourceConflictException(
+ "username '" + username + "' already exists");
+ }
+ if (input.email != null
+ && db.accountExternalIds().get(getEmailKey(input.email)) != null) {
+ throw new UnprocessableEntityException(
+ "email '" + input.email + "' already exists");
+ }
+
+ try {
+ db.accountExternalIds().insert(Collections.singleton(extUser));
+ } catch (OrmDuplicateKeyException duplicateKey) {
+ throw new ResourceConflictException(
+ "username '" + username + "' already exists");
+ }
+
+ if (input.email != null) {
+ AccountExternalId extMailto =
+ new AccountExternalId(id, getEmailKey(input.email));
+ extMailto.setEmailAddress(input.email);
+ try {
+ db.accountExternalIds().insert(Collections.singleton(extMailto));
+ } catch (OrmDuplicateKeyException duplicateKey) {
+ try {
+ db.accountExternalIds().delete(Collections.singleton(extUser));
+ } catch (OrmException cleanupError) {
+ }
+ throw new UnprocessableEntityException(
+ "email '" + input.email + "' already exists");
+ }
+ }
+
+ Account a = new Account(id);
+ a.setFullName(input.name);
+ a.setPreferredEmail(input.email);
+ db.accounts().insert(Collections.singleton(a));
+
+ if (key != null) {
+ db.accountSshKeys().insert(Collections.singleton(key));
+ }
+
+ for (AccountGroup.Id groupId : groups) {
+ AccountGroupMember m =
+ new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
+ db.accountGroupMembersAudit().insert(Collections.singleton(
+ new AccountGroupMemberAudit(m, currentUser.getAccountId())));
+ db.accountGroupMembers().insert(Collections.singleton(m));
+ }
+
+ sshKeyCache.evict(username);
+ accountCache.evictByUsername(username);
+ byEmailCache.evict(input.email);
+
+ AccountInfo.Loader loader = infoLoader.create(true);
+ AccountInfo info = loader.get(id);
+ loader.fill();
+ return Response.created(info);
+ }
+
+ private Set<AccountGroup.Id> parseGroups(List<String> groups)
+ throws UnprocessableEntityException {
+ Set<AccountGroup.Id> groupIds = Sets.newHashSet();
+ if (groups != null) {
+ for (String g : groups) {
+ groupIds.add(GroupDescriptions.toAccountGroup(
+ groupsCollection.parseInternal(g)).getId());
+ }
+ }
+ return groupIds;
+ }
+
+ private AccountSshKey createSshKey(Account.Id id, String sshKey)
+ throws BadRequestException {
+ if (sshKey == null) {
+ return null;
+ }
+ try {
+ return sshKeyCache.create(new AccountSshKey.Id(id, 1), sshKey.trim());
+ } catch (InvalidSshKeyException e) {
+ throw new BadRequestException(e.getMessage());
+ }
+ }
+
+ private AccountExternalId.Key getEmailKey(String email) {
+ return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
new file mode 100644
index 0000000..4fda74c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CreateEmail.Input;
+import com.google.gerrit.server.account.GetEmails.EmailInfo;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.mail.RegisterNewEmailSender;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CreateEmail implements RestModifyView<AccountResource, Input> {
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ public static class Input {
+ @DefaultInput
+ public String email;
+ public boolean preferred;
+ public boolean noConfirmation;
+ }
+
+ public static interface Factory {
+ CreateEmail create(String email);
+ }
+
+ private final Provider<CurrentUser> self;
+ private final Realm realm;
+ private final AuthConfig authConfig;
+ private final AccountManager accountManager;
+ private final RegisterNewEmailSender.Factory registerNewEmailFactory;
+ private final Provider<PutPreferred> putPreferredProvider;
+ private final String email;
+
+ @Inject
+ CreateEmail(Provider<CurrentUser> self,
+ Realm realm,
+ AuthConfig authConfig,
+ AccountManager accountManager,
+ RegisterNewEmailSender.Factory registerNewEmailFactory,
+ Provider<PutPreferred> putPreferredProvider,
+ @Assisted String email) {
+ this.self = self;
+ this.realm = realm;
+ this.authConfig = authConfig;
+ this.accountManager = accountManager;
+ this.registerNewEmailFactory = registerNewEmailFactory;
+ this.putPreferredProvider = putPreferredProvider;
+ this.email = email;
+ }
+
+ @Override
+ public Object apply(AccountResource rsrc, Input input) throws AuthException,
+ BadRequestException, ResourceConflictException,
+ ResourceNotFoundException, OrmException, EmailException,
+ MethodNotAllowedException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("not allowed to add email address");
+ }
+
+ if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
+ throw new MethodNotAllowedException("realm does not allow adding emails");
+ }
+
+ if (input == null) {
+ input = new Input();
+ }
+
+ if (input.email != null && !email.equals(input.email)) {
+ throw new BadRequestException("email address must match URL");
+ }
+
+ if (input.noConfirmation
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("must be administrator to use no_confirmation");
+ }
+
+ EmailInfo info = new EmailInfo();
+ info.email = email;
+ if (input.noConfirmation
+ || authConfig.getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
+ try {
+ accountManager.link(rsrc.getUser().getAccountId(),
+ AuthRequest.forEmail(email));
+ } catch (AccountException e) {
+ throw new ResourceConflictException(e.getMessage());
+ }
+ if (input.preferred) {
+ putPreferredProvider.get().apply(
+ new AccountResource.Email(rsrc.getUser(), email),
+ null);
+ info.preferred = true;
+ }
+ } else {
+ try {
+ registerNewEmailFactory.create(email).send();
+ info.pendingConfirmation = true;
+ } catch (EmailException e) {
+ log.error("Cannot send email verification message to " + email, e);
+ throw e;
+ } catch (RuntimeException e) {
+ log.error("Cannot send email verification message to " + email, e);
+ throw e;
+ }
+ }
+ return Response.created(info);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index c90f3e9..d9fb303 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -14,8 +14,11 @@
package com.google.gerrit.server.account;
+import com.google.common.base.Strings;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.Inject;
import java.util.Set;
@@ -23,17 +26,32 @@
public class DefaultRealm implements Realm {
private final EmailExpander emailExpander;
private final AccountByEmailCache byEmail;
+ private final AuthConfig authConfig;
@Inject
DefaultRealm(final EmailExpander emailExpander,
- final AccountByEmailCache byEmail) {
+ final AccountByEmailCache byEmail, final AuthConfig authConfig) {
this.emailExpander = emailExpander;
this.byEmail = byEmail;
+ this.authConfig = authConfig;
}
@Override
public boolean allowsEdit(final Account.FieldName field) {
- return true;
+ if (authConfig.getAuthType() == AuthType.HTTP) {
+ switch (field) {
+ case USER_NAME:
+ return false;
+ case FULL_NAME:
+ return Strings.emptyToNull(authConfig.getHttpDisplaynameHeader()) == null;
+ case REGISTER_NEW_EMAIL:
+ return Strings.emptyToNull(authConfig.getHttpEmailHeader()) == null;
+ default:
+ return true;
+ }
+ } else {
+ return true;
+ }
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
new file mode 100644
index 0000000..d44bc2c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.DeleteActive.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+public class DeleteActive implements RestModifyView<AccountResource, Input> {
+ public static class Input {
+ }
+
+ private final Provider<ReviewDb> dbProvider;
+ private final AccountCache byIdCache;
+
+ @Inject
+ DeleteActive(Provider<ReviewDb> dbProvider, AccountCache byIdCache) {
+ this.dbProvider = dbProvider;
+ this.byIdCache = byIdCache;
+ }
+
+ @Override
+ public Object apply(AccountResource rsrc, Input input)
+ throws ResourceNotFoundException, OrmException {
+ Account a = dbProvider.get().accounts().get(rsrc.getUser().getAccountId());
+ if (a == null) {
+ throw new ResourceNotFoundException("account not found");
+ }
+ if (!a.isActive()) {
+ throw new ResourceNotFoundException();
+ }
+ a.setActive(false);
+ dbProvider.get().accounts().update(Collections.singleton(a));
+ byIdCache.evict(a.getId());
+ return Response.none();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
new file mode 100644
index 0000000..4b38b9f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.DeleteEmail.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class DeleteEmail implements RestModifyView<AccountResource.Email, Input> {
+ public static class Input {
+ }
+
+ private final Provider<CurrentUser> self;
+ private final Realm realm;
+ private final Provider<ReviewDb> dbProvider;
+ private final AccountManager accountManager;
+
+ @Inject
+ DeleteEmail(Provider<CurrentUser> self, Realm realm,
+ Provider<ReviewDb> dbProvider, AccountManager accountManager) {
+ this.self = self;
+ this.realm = realm;
+ this.dbProvider = dbProvider;
+ this.accountManager = accountManager;
+ }
+
+ @Override
+ public Object apply(AccountResource.Email rsrc, Input input)
+ throws AuthException, ResourceNotFoundException,
+ ResourceConflictException, MethodNotAllowedException, OrmException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("not allowed to delete email address");
+ }
+ if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
+ throw new MethodNotAllowedException("realm does not allow deleting emails");
+ }
+ AccountExternalId.Key key = new AccountExternalId.Key(
+ AccountExternalId.SCHEME_MAILTO, rsrc.getEmail());
+ AccountExternalId extId = dbProvider.get().accountExternalIds().get(key);
+ if (extId == null) {
+ throw new ResourceNotFoundException(rsrc.getEmail());
+ }
+ try {
+ accountManager.unlink(rsrc.getUser().getAccountId(),
+ AuthRequest.forEmail(rsrc.getEmail()));
+ } catch (AccountException e) {
+ throw new ResourceConflictException(e.getMessage());
+ }
+ return Response.none();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
new file mode 100644
index 0000000..cf60df1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.DeleteSshKey.Input;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+public class DeleteSshKey implements
+ RestModifyView<AccountResource.SshKey, Input> {
+ public static class Input {
+ }
+
+ private final Provider<ReviewDb> dbProvider;
+ private final SshKeyCache sshKeyCache;
+
+ @Inject
+ DeleteSshKey(Provider<ReviewDb> dbProvider, SshKeyCache sshKeyCache) {
+ this.dbProvider = dbProvider;
+ this.sshKeyCache = sshKeyCache;
+ }
+
+ @Override
+ public Object apply(AccountResource.SshKey rsrc, Input input)
+ throws OrmException {
+ dbProvider.get().accountSshKeys()
+ .deleteKeys(Collections.singleton(rsrc.getSshKey().getKey()));
+ sshKeyCache.evict(rsrc.getUser().getUserName());
+ return Response.none();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
new file mode 100644
index 0000000..f523e15
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountResource.Email;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Emails implements
+ ChildCollection<AccountResource, AccountResource.Email>,
+ AcceptsCreate<AccountResource> {
+ private final DynamicMap<RestView<AccountResource.Email>> views;
+ private final Provider<GetEmails> list;
+ private final Provider<CurrentUser> self;
+ private final CreateEmail.Factory createEmailFactory;
+
+ @Inject
+ Emails(DynamicMap<RestView<AccountResource.Email>> views,
+ Provider<GetEmails> list,
+ Provider<CurrentUser> self,
+ CreateEmail.Factory createEmailFactory) {
+ this.views = views;
+ this.list = list;
+ this.self = self;
+ this.createEmailFactory = createEmailFactory;
+ }
+
+ @Override
+ public RestView<AccountResource> list() {
+ return list.get();
+ }
+
+ @Override
+ public AccountResource.Email parse(AccountResource rsrc, IdString id)
+ throws ResourceNotFoundException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new ResourceNotFoundException();
+ }
+
+ if ("preferred".equals(id.get())) {
+ String email = rsrc.getUser().getAccount().getPreferredEmail();
+ if (Strings.isNullOrEmpty(email)) {
+ throw new ResourceNotFoundException();
+ }
+ return new AccountResource.Email(rsrc.getUser(), email);
+ } else if (rsrc.getUser().getEmailAddresses().contains(id.get())) {
+ return new AccountResource.Email(rsrc.getUser(), id.get());
+ } else {
+ throw new ResourceNotFoundException();
+ }
+ }
+
+ @Override
+ public DynamicMap<RestView<Email>> views() {
+ return views;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public CreateEmail create(AccountResource parent, IdString email) {
+ return createEmailFactory.create(email.get());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java
deleted file mode 100644
index bbab126..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.account;
-
-import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.apache.commons.codec.binary.Base64;
-
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Collections;
-import java.util.concurrent.Callable;
-
-/** Operation to generate a password for an account. */
-public class GeneratePassword implements Callable<AccountExternalId> {
- private static final int LEN = 12;
- private static final SecureRandom rng;
-
- static {
- try {
- rng = SecureRandom.getInstance("SHA1PRNG");
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException("Cannot create RNG for password generator", e);
- }
- }
-
- public interface Factory {
- GeneratePassword create(AccountExternalId.Key forUser);
- }
-
- private final AccountCache accountCache;
- private final ReviewDb db;
- private final IdentifiedUser user;
-
- private final AccountExternalId.Key forUser;
-
- @Inject
- GeneratePassword(final AccountCache accountCache, final ReviewDb db,
- final IdentifiedUser user,
-
- @Assisted AccountExternalId.Key forUser) {
- this.accountCache = accountCache;
- this.db = db;
- this.user = user;
-
- this.forUser = forUser;
- }
-
- public AccountExternalId call() throws OrmException, NoSuchEntityException {
- AccountExternalId id = db.accountExternalIds().get(forUser);
- if (id == null || !user.getAccountId().equals(id.getAccountId())) {
- throw new NoSuchEntityException();
- }
-
- id.setPassword(generate());
- db.accountExternalIds().update(Collections.singleton(id));
- accountCache.evict(user.getAccountId());
- return id;
- }
-
- private String generate() {
- byte[] rand = new byte[LEN];
- rng.nextBytes(rand);
-
- byte[] enc = Base64.encodeBase64(rand, false);
- StringBuilder r = new StringBuilder(LEN);
- for (int i = 0; i < LEN; i++) {
- if (enc[i] == '=') {
- break;
- }
- r.append((char) enc[i]);
- }
- return r.toString();
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
index b022420..f990b5b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
@@ -15,10 +15,22 @@
package com.google.gerrit.server.account;
import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
public class GetAccount implements RestReadView<AccountResource> {
+ private final AccountInfo.Loader.Factory infoFactory;
+
+ @Inject
+ GetAccount(AccountInfo.Loader.Factory infoFactory) {
+ this.infoFactory = infoFactory;
+ }
+
@Override
- public AccountInfo apply(AccountResource rsrc) {
- return AccountInfo.parse(rsrc.getUser().getAccount(), true);
+ public AccountInfo apply(AccountResource rsrc) throws OrmException {
+ AccountInfo.Loader loader = infoFactory.create(true);
+ AccountInfo info = loader.get(rsrc.getUser().getAccountId());
+ loader.fill();
+ return info;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java
new file mode 100644
index 0000000..76c7ddb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+
+public class GetActive implements RestReadView<AccountResource> {
+ @Override
+ public Object apply(AccountResource rsrc) throws ResourceNotFoundException {
+ if (rsrc.getUser().getAccount().isActive()) {
+ return Response.ok("");
+ }
+ throw new ResourceNotFoundException();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java
index 1c66555..a96e713 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java
@@ -16,6 +16,7 @@
import com.google.common.base.Strings;
import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.CacheControl;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
@@ -24,6 +25,8 @@
import org.kohsuke.args4j.Option;
+import java.util.concurrent.TimeUnit;
+
class GetAvatar implements RestReadView<AccountResource> {
private final DynamicItem<AvatarProvider> avatarProvider;
@@ -41,12 +44,14 @@
throws ResourceNotFoundException {
AvatarProvider impl = avatarProvider.get();
if (impl == null) {
- throw new ResourceNotFoundException();
+ throw (new ResourceNotFoundException())
+ .caching(CacheControl.PUBLIC(1, TimeUnit.DAYS));
}
String url = impl.getUrl(rsrc.getUser(), size);
if (Strings.isNullOrEmpty(url)) {
- throw new ResourceNotFoundException();
+ throw (new ResourceNotFoundException())
+ .caching(CacheControl.PUBLIC(1, TimeUnit.HOURS));
} else {
return Response.redirect(url);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index 54f1980..615d09e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -23,7 +23,6 @@
import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
import static com.google.gerrit.common.data.GlobalCapability.RUN_GC;
-import static com.google.gerrit.common.data.GlobalCapability.START_REPLICATION;
import static com.google.gerrit.common.data.GlobalCapability.STREAM_EVENTS;
import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS;
@@ -34,6 +33,8 @@
import com.google.common.collect.Sets;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -68,10 +69,13 @@
private Set<String> query;
private final Provider<CurrentUser> self;
+ private final DynamicMap<CapabilityDefinition> pluginCapabilities;
@Inject
- GetCapabilities(Provider<CurrentUser> self) {
+ GetCapabilities(Provider<CurrentUser> self,
+ DynamicMap<CapabilityDefinition> pluginCapabilities) {
this.self = self;
+ this.pluginCapabilities = pluginCapabilities;
}
@Override
@@ -93,6 +97,14 @@
}
}
}
+ for (String pluginName : pluginCapabilities.plugins()) {
+ for (String capability : pluginCapabilities.byPlugin(pluginName).keySet()) {
+ String name = String.format("%s-%s", pluginName, capability);
+ if (want(name) && cc.canPerform(name)) {
+ have.put(name, true);
+ }
+ }
+ }
have.put(CREATE_ACCOUNT, cc.canCreateAccount());
have.put(CREATE_GROUP, cc.canCreateGroup());
@@ -104,7 +116,6 @@
have.put(VIEW_CONNECTIONS, cc.canViewConnections());
have.put(VIEW_QUEUE, cc.canViewQueue());
have.put(RUN_GC, cc.canRunGC());
- have.put(START_REPLICATION, cc.canStartReplication());
have.put(STREAM_EVENTS, cc.canStreamEvents());
have.put(ACCESS_DATABASE, cc.canAccessDatabase());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmail.java
new file mode 100644
index 0000000..c56a0a0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmail.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.GetEmails.EmailInfo;
+
+public class GetEmail implements RestReadView<AccountResource.Email> {
+ @Override
+ public EmailInfo apply(AccountResource.Email rsrc) {
+ EmailInfo e = new EmailInfo();
+ e.email = rsrc.getEmail();
+ e.preferred(rsrc.getUser().getAccount().getPreferredEmail());
+ return e;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
new file mode 100644
index 0000000..0e0e77a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEmails.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class GetEmails implements RestReadView<AccountResource> {
+ private final Provider<CurrentUser> self;
+
+ @Inject
+ public GetEmails(Provider<CurrentUser> self) {
+ this.self = self;
+ }
+
+ @Override
+ public List<EmailInfo> apply(AccountResource rsrc) throws AuthException,
+ OrmException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("not allowed to list email addresses");
+ }
+
+ List<EmailInfo> emails = Lists.newArrayList();
+ for (String email : rsrc.getUser().getEmailAddresses()) {
+ if (email != null) {
+ EmailInfo e = new EmailInfo();
+ e.email = email;
+ e.preferred(rsrc.getUser().getAccount().getPreferredEmail());
+ emails.add(e);
+ }
+ }
+ Collections.sort(emails, new Comparator<EmailInfo>() {
+ @Override
+ public int compare(EmailInfo a, EmailInfo b) {
+ return a.email.compareTo(b.email);
+ }
+ });
+ return emails;
+ }
+
+ public static class EmailInfo {
+ public String email;
+ public Boolean preferred;
+ public Boolean pendingConfirmation;
+
+ void preferred(String e) {
+ this.preferred = e != null && e.equals(email) ? true : null;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java
new file mode 100644
index 0000000..8eaf4b3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetHttpPassword.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class GetHttpPassword implements RestReadView<AccountResource> {
+
+ private final Provider<CurrentUser> self;
+
+ @Inject
+ GetHttpPassword(Provider<CurrentUser> self) {
+ this.self = self;
+ }
+
+ @Override
+ public String apply(AccountResource rsrc) throws AuthException,
+ ResourceNotFoundException, OrmException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("not allowed to get http password");
+ }
+ AccountState s = rsrc.getUser().state();
+ if (s.getUserName() == null) {
+ throw new ResourceNotFoundException();
+ }
+ String p = s.getPassword(s.getUserName());
+ if (p == null) {
+ throw new ResourceNotFoundException();
+ }
+ return p;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetName.java
new file mode 100644
index 0000000..646a3b2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetName.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.RestReadView;
+
+public class GetName implements RestReadView<AccountResource> {
+ @Override
+ public String apply(AccountResource rsrc) {
+ return Strings.nullToEmpty(rsrc.getUser().getAccount().getFullName());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
new file mode 100644
index 0000000..733c49d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class GetPreferences implements RestReadView<AccountResource> {
+ private final Provider<CurrentUser> self;
+
+ @Inject
+ GetPreferences(Provider<CurrentUser> self) {
+ this.self = self;
+ }
+
+ @Override
+ public PreferenceInfo apply(AccountResource rsrc) throws AuthException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("restricted to administrator");
+ }
+ return new PreferenceInfo(rsrc.getUser().getAccount()
+ .getGeneralPreferences());
+ }
+
+ static class PreferenceInfo {
+ final String kind = "gerritcodereview#preferences";
+
+ short changesPerPage;
+ Boolean showSiteHeader;
+ Boolean useFlashClipboard;
+ DownloadScheme downloadScheme;
+ DownloadCommand downloadCommand;
+ Boolean copySelfOnEmail;
+ DateFormat dateFormat;
+ TimeFormat timeFormat;
+ Boolean reversePatchSetOrder;
+ Boolean showUsernameInReviewCategory;
+ Boolean relativeDateInChangeTable;
+ CommentVisibilityStrategy commentVisibilityStrategy;
+ DiffView diffView;
+
+ PreferenceInfo(AccountGeneralPreferences p) {
+ changesPerPage = p.getMaximumPageSize();
+ showSiteHeader = p.isShowSiteHeader() ? true : null;
+ useFlashClipboard = p.isUseFlashClipboard() ? true : null;
+ downloadScheme = p.getDownloadUrl();
+ downloadCommand = p.getDownloadCommand();
+ copySelfOnEmail = p.isCopySelfOnEmails() ? true : null;
+ dateFormat = p.getDateFormat();
+ timeFormat = p.getTimeFormat();
+ reversePatchSetOrder = p.isReversePatchSetOrder() ? true : null;
+ showUsernameInReviewCategory = p.isShowUsernameInReviewCategory() ? true : null;
+ relativeDateInChangeTable = p.isRelativeDateInChangeTable() ? true : null;
+ commentVisibilityStrategy = p.getCommentVisibilityStrategy();
+ diffView = p.getDiffView();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKey.java
new file mode 100644
index 0000000..37445e9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKey.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.AccountResource.SshKey;
+import com.google.gerrit.server.account.GetSshKeys.SshKeyInfo;
+
+public class GetSshKey implements RestReadView<AccountResource.SshKey> {
+
+ @Override
+ public SshKeyInfo apply(SshKey rsrc) {
+ return new SshKeyInfo(rsrc.getSshKey());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
new file mode 100644
index 0000000..f198b77
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.List;
+
+public class GetSshKeys implements RestReadView<AccountResource> {
+
+ private final Provider<CurrentUser> self;
+ private final Provider<ReviewDb> dbProvider;
+
+ @Inject
+ GetSshKeys(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider) {
+ this.self = self;
+ this.dbProvider = dbProvider;
+ }
+
+ @Override
+ public List<SshKeyInfo> apply(AccountResource rsrc) throws AuthException,
+ OrmException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("not allowed to get SSH keys");
+ }
+
+ List<SshKeyInfo> sshKeys = Lists.newArrayList();
+ for (AccountSshKey sshKey : dbProvider.get().accountSshKeys()
+ .byAccount(rsrc.getUser().getAccountId()).toList()) {
+ sshKeys.add(new SshKeyInfo(sshKey));
+ }
+ return sshKeys;
+ }
+
+ public static class SshKeyInfo {
+ public SshKeyInfo(AccountSshKey sshKey) {
+ seq = sshKey.getKey().get();
+ sshPublicKey = sshKey.getSshPublicKey();
+ encodedKey = sshKey.getEncodedKey();
+ algorithm = sshKey.getAlgorithm();
+ comment = Strings.emptyToNull(sshKey.getComment());
+ valid = sshKey.isValid();
+ }
+
+ public int seq;
+ public String sshPublicKey;
+ public String encodedKey;
+ public String algorithm;
+ public String comment;
+ public boolean valid;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetUsername.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetUsername.java
new file mode 100644
index 0000000..8dcb236
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetUsername.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class GetUsername implements RestReadView<AccountResource> {
+
+ private final Provider<CurrentUser> self;
+
+ @Inject
+ GetUsername(Provider<CurrentUser> self) {
+ this.self = self;
+ }
+
+ @Override
+ public String apply(AccountResource rsrc) throws AuthException,
+ ResourceNotFoundException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("not allowed to get username");
+ }
+ String username = rsrc.getUser().getAccount().getUserName();
+ if (username == null) {
+ throw new ResourceNotFoundException();
+ }
+ return username;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index 2e01f26..ede2431 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -159,7 +159,7 @@
}
public boolean canSeeMember(Account.Id id) {
- if (user instanceof IdentifiedUser
+ if (user.isIdentifiedUser()
&& ((IdentifiedUser) user).getAccountId().equals(id)) {
return true;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
index cade9c7..4d74e4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
@@ -20,7 +20,7 @@
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.server.OrmException;
@@ -122,19 +122,19 @@
return members;
}
- private List<AccountGroupIncludeByUuid> loadIncludes() throws OrmException {
- List<AccountGroupIncludeByUuid> groups = new ArrayList<AccountGroupIncludeByUuid>();
+ private List<AccountGroupById> loadIncludes() throws OrmException {
+ List<AccountGroupById> groups = new ArrayList<AccountGroupById>();
- for (final AccountGroupIncludeByUuid m : db.accountGroupIncludesByUuid().byGroup(groupId)) {
+ for (final AccountGroupById m : db.accountGroupById().byGroup(groupId)) {
if (control.canSeeGroup(m.getIncludeUUID())) {
gic.want(m.getIncludeUUID());
groups.add(m);
}
}
- Collections.sort(groups, new Comparator<AccountGroupIncludeByUuid>() {
- public int compare(final AccountGroupIncludeByUuid o1,
- final AccountGroupIncludeByUuid o2) {
+ Collections.sort(groups, new Comparator<AccountGroupById>() {
+ public int compare(final AccountGroupById o1,
+ final AccountGroupById o2) {
GroupDescription.Basic a = gic.get(o1.getIncludeUUID());
GroupDescription.Basic b = gic.get(o2.getIncludeUUID());
return n(a).compareTo(n(b));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index bbcdfaf..37d407c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -19,7 +19,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gwtorm.server.SchemaFactory;
@@ -152,7 +152,7 @@
}
Set<AccountGroup.UUID> ids = Sets.newHashSet();
- for (AccountGroupIncludeByUuid agi : db.accountGroupIncludesByUuid()
+ for (AccountGroupById agi : db.accountGroupById()
.byGroup(group.get(0).getId())) {
ids.add(agi.getIncludeUUID());
}
@@ -177,7 +177,7 @@
final ReviewDb db = schema.open();
try {
Set<AccountGroup.Id> ids = Sets.newHashSet();
- for (AccountGroupIncludeByUuid agi : db.accountGroupIncludesByUuid()
+ for (AccountGroupById agi : db.accountGroupById()
.byIncludeUUID(key)) {
ids.add(agi.getGroupId());
}
@@ -207,7 +207,7 @@
final ReviewDb db = schema.open();
try {
Set<AccountGroup.UUID> ids = Sets.newHashSet();
- for (AccountGroupIncludeByUuid agi : db.accountGroupIncludesByUuid().all()) {
+ for (AccountGroupById agi : db.accountGroupById().all()) {
if (!AccountGroup.isInternalGroup(agi.getIncludeUUID())) {
ids.add(agi.getIncludeUUID());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
index 08cf1a7..b6f7b488 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
@@ -18,7 +18,7 @@
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
@@ -28,6 +28,7 @@
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@@ -58,13 +59,13 @@
public Set<Account> listAccounts(final AccountGroup.UUID groupUUID,
final Project.NameKey project) throws NoSuchGroupException,
- NoSuchProjectException, OrmException {
+ NoSuchProjectException, OrmException, IOException {
return listAccounts(groupUUID, project, new HashSet<AccountGroup.UUID>());
}
private Set<Account> listAccounts(final AccountGroup.UUID groupUUID,
final Project.NameKey project, final Set<AccountGroup.UUID> seen)
- throws NoSuchGroupException, OrmException, NoSuchProjectException {
+ throws NoSuchGroupException, OrmException, NoSuchProjectException, IOException {
if (AccountGroup.PROJECT_OWNERS.equals(groupUUID)) {
return getProjectOwners(project, seen);
} else {
@@ -79,7 +80,7 @@
private Set<Account> getProjectOwners(final Project.NameKey project,
final Set<AccountGroup.UUID> seen) throws NoSuchProjectException,
- NoSuchGroupException, OrmException {
+ NoSuchGroupException, OrmException, IOException {
seen.add(AccountGroup.PROJECT_OWNERS);
if (project == null) {
return Collections.emptySet();
@@ -100,7 +101,7 @@
private Set<Account> getGroupMembers(final AccountGroup group,
final Project.NameKey project, final Set<AccountGroup.UUID> seen)
- throws NoSuchGroupException, OrmException, NoSuchProjectException {
+ throws NoSuchGroupException, OrmException, NoSuchProjectException, IOException {
seen.add(group.getGroupUUID());
final GroupDetail groupDetail =
groupDetailFactory.create(group.getId()).call();
@@ -112,7 +113,7 @@
}
}
if (groupDetail.includes != null) {
- for (final AccountGroupIncludeByUuid groupInclude : groupDetail.includes) {
+ for (final AccountGroupById groupInclude : groupDetail.includes) {
final AccountGroup includedGroup =
groupCache.get(groupInclude.getIncludeUUID());
if (includedGroup != null && !seen.contains(includedGroup.getGroupUUID())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
new file mode 100644
index 0000000..807fa1f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -0,0 +1,119 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountInfo.AvatarInfo;
+import com.google.gerrit.server.avatar.AvatarProvider;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.Set;
+
+@Singleton
+public class InternalAccountDirectory extends AccountDirectory {
+ public static class Module extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(AccountDirectory.class).to(InternalAccountDirectory.class);
+ }
+ }
+
+ private final Provider<ReviewDb> db;
+ private final AccountCache accountCache;
+ private final DynamicItem<AvatarProvider> avatar;
+ private final IdentifiedUser.GenericFactory userFactory;
+
+ @Inject
+ InternalAccountDirectory(Provider<ReviewDb> db,
+ AccountCache accountCache,
+ DynamicItem<AvatarProvider> avatar,
+ IdentifiedUser.GenericFactory userFactory) {
+ this.db = db;
+ this.accountCache = accountCache;
+ this.avatar = avatar;
+ this.userFactory = userFactory;
+ }
+
+ @Override
+ public void fillAccountInfo(
+ Iterable<? extends AccountInfo> in,
+ Set<FillOptions> options)
+ throws DirectoryException {
+ Multimap<Account.Id, AccountInfo> missing = ArrayListMultimap.create();
+ for (AccountInfo info : in) {
+ AccountState state = accountCache.getIfPresent(info._id);
+ if (state != null) {
+ fill(info, state.getAccount(), options);
+ } else {
+ missing.put(info._id, info);
+ }
+ }
+ if (!missing.isEmpty()) {
+ try {
+ for (Account account : db.get().accounts().get(missing.keySet())) {
+ for (AccountInfo info : missing.get(account.getId())) {
+ fill(info, account, options);
+ }
+ }
+ } catch (OrmException e) {
+ throw new DirectoryException(e);
+ }
+ }
+ }
+
+ private void fill(AccountInfo info,
+ Account account,
+ Set<FillOptions> options) {
+ if (options.contains(FillOptions.NAME)) {
+ info.name = Strings.emptyToNull(account.getFullName());
+ if (info.name == null) {
+ info.name = account.getUserName();
+ }
+ }
+ if (options.contains(FillOptions.EMAIL)) {
+ info.email = account.getPreferredEmail();
+ }
+ if (options.contains(FillOptions.USERNAME)) {
+ info.username = account.getUserName();
+ }
+ if (options.contains(FillOptions.AVATARS)) {
+ info.avatars = Lists.newArrayListWithCapacity(1);
+ AvatarProvider ap = avatar.get();
+ if (ap != null) {
+ String u = ap.getUrl(
+ userFactory.create(account.getId()),
+ AvatarInfo.DEFAULT_SIZE);
+ if (u != null) {
+ AvatarInfo a = new AvatarInfo();
+ a.url = u;
+ a.height = AvatarInfo.DEFAULT_SIZE;
+ info.avatars.add(a);
+ }
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
index 57a4a22..79a0089 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
@@ -16,9 +16,12 @@
import static com.google.gerrit.server.account.AccountResource.ACCOUNT_KIND;
import static com.google.gerrit.server.account.AccountResource.CAPABILITY_KIND;
+import static com.google.gerrit.server.account.AccountResource.EMAIL_KIND;
+import static com.google.gerrit.server.account.AccountResource.SSH_KEY_KIND;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
public class Module extends RestApiModule {
@Override
@@ -27,15 +30,42 @@
bind(Capabilities.class);
DynamicMap.mapOf(binder(), ACCOUNT_KIND);
+ DynamicMap.mapOf(binder(), EMAIL_KIND);
+ DynamicMap.mapOf(binder(), SSH_KEY_KIND);
DynamicMap.mapOf(binder(), CAPABILITY_KIND);
+ put(ACCOUNT_KIND).to(PutAccount.class);
get(ACCOUNT_KIND).to(GetAccount.class);
+ get(ACCOUNT_KIND, "name").to(GetName.class);
+ put(ACCOUNT_KIND, "name").to(PutName.class);
+ delete(ACCOUNT_KIND, "name").to(PutName.class);
+ get(ACCOUNT_KIND, "username").to(GetUsername.class);
+ get(ACCOUNT_KIND, "active").to(GetActive.class);
+ put(ACCOUNT_KIND, "active").to(PutActive.class);
+ delete(ACCOUNT_KIND, "active").to(DeleteActive.class);
+ child(ACCOUNT_KIND, "emails").to(Emails.class);
+ get(EMAIL_KIND).to(GetEmail.class);
+ put(EMAIL_KIND).to(PutEmail.class);
+ delete(EMAIL_KIND).to(DeleteEmail.class);
+ put(EMAIL_KIND, "preferred").to(PutPreferred.class);
+ get(ACCOUNT_KIND, "password.http").to(GetHttpPassword.class);
+ put(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
+ delete(ACCOUNT_KIND, "password.http").to(PutHttpPassword.class);
+ child(ACCOUNT_KIND, "sshkeys").to(SshKeys.class);
+ post(ACCOUNT_KIND, "sshkeys").to(AddSshKey.class);
+ get(SSH_KEY_KIND).to(GetSshKey.class);
+ delete(SSH_KEY_KIND).to(DeleteSshKey.class);
get(ACCOUNT_KIND, "avatar").to(GetAvatar.class);
get(ACCOUNT_KIND, "avatar.change.url").to(GetAvatarChangeUrl.class);
child(ACCOUNT_KIND, "capabilities").to(Capabilities.class);
get(ACCOUNT_KIND, "groups").to(GetGroups.class);
+ get(ACCOUNT_KIND, "preferences").to(GetPreferences.class);
+ post(ACCOUNT_KIND, "preferences").to(SetPreferences.class);
get(ACCOUNT_KIND, "preferences.diff").to(GetDiffPreferences.class);
put(ACCOUNT_KIND, "preferences.diff").to(SetDiffPreferences.class);
get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
+
+ install(new FactoryModuleBuilder().build(CreateAccount.Factory.class));
+ install(new FactoryModuleBuilder().build(CreateEmail.Factory.class));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
index 8b966cb..9dc0d5a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
@@ -18,8 +18,8 @@
import com.google.gerrit.common.errors.PermissionDeniedException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
import com.google.gerrit.reviewdb.client.AccountGroupName;
@@ -161,21 +161,21 @@
private void addGroups(final AccountGroup.Id groupId,
final Collection<? extends AccountGroup.UUID> groups) throws OrmException {
- final List<AccountGroupIncludeByUuid> includeList =
- new ArrayList<AccountGroupIncludeByUuid>();
- final List<AccountGroupIncludeByUuidAudit> includesAudit =
- new ArrayList<AccountGroupIncludeByUuidAudit>();
+ final List<AccountGroupById> includeList =
+ new ArrayList<AccountGroupById>();
+ final List<AccountGroupByIdAud> includesAudit =
+ new ArrayList<AccountGroupByIdAud>();
for (AccountGroup.UUID includeUUID : groups) {
- final AccountGroupIncludeByUuid groupInclude =
- new AccountGroupIncludeByUuid(new AccountGroupIncludeByUuid.Key(groupId, includeUUID));
+ final AccountGroupById groupInclude =
+ new AccountGroupById(new AccountGroupById.Key(groupId, includeUUID));
includeList.add(groupInclude);
- final AccountGroupIncludeByUuidAudit audit =
- new AccountGroupIncludeByUuidAudit(groupInclude, currentUser.getAccountId());
+ final AccountGroupByIdAud audit =
+ new AccountGroupByIdAud(groupInclude, currentUser.getAccountId());
includesAudit.add(audit);
}
- db.accountGroupIncludesByUuid().insert(includeList);
- db.accountGroupIncludesByUuidAudit().insert(includesAudit);
+ db.accountGroupById().insert(includeList);
+ db.accountGroupByIdAud().insert(includesAudit);
for (AccountGroup.UUID uuid : groups) {
groupIncludeCache.evictMemberIn(uuid);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
new file mode 100644
index 0000000..f7584ed
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.CreateAccount.Input;
+
+public class PutAccount implements RestModifyView<AccountResource, Input> {
+ @Override
+ public Object apply(AccountResource resource, Input input)
+ throws ResourceConflictException {
+ throw new ResourceConflictException("account exists");
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
new file mode 100644
index 0000000..a860fda
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.PutActive.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+public class PutActive implements RestModifyView<AccountResource, Input> {
+ public static class Input {
+ }
+
+ private final Provider<ReviewDb> dbProvider;
+ private final AccountCache byIdCache;
+
+ @Inject
+ PutActive(Provider<ReviewDb> dbProvider, AccountCache byIdCache) {
+ this.dbProvider = dbProvider;
+ this.byIdCache = byIdCache;
+ }
+
+ @Override
+ public Object apply(AccountResource rsrc, Input input)
+ throws ResourceNotFoundException, OrmException {
+ Account a = dbProvider.get().accounts().get(rsrc.getUser().getAccountId());
+ if (a == null) {
+ throw new ResourceNotFoundException("account not found");
+ }
+ if (a.isActive()) {
+ return Response.ok("");
+ }
+ a.setActive(true);
+ dbProvider.get().accounts().update(Collections.singleton(a));
+ byIdCache.evict(a.getId());
+ return Response.created("");
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java
new file mode 100644
index 0000000..b79d8dc4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutEmail.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.CreateEmail.Input;
+
+public class PutEmail implements RestModifyView<AccountResource.Email, Input> {
+ @Override
+ public Object apply(AccountResource.Email rsrc, Input input)
+ throws ResourceConflictException {
+ throw new ResourceConflictException("email exists");
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
new file mode 100644
index 0000000..0601b8d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
@@ -0,0 +1,132 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.PutHttpPassword.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.commons.codec.binary.Base64;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Collections;
+
+public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
+ public static class Input {
+ public String httpPassword;
+ public boolean generate;
+ }
+
+ private static final int LEN = 12;
+ private static final SecureRandom rng;
+
+ static {
+ try {
+ rng = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("Cannot create RNG for password generator", e);
+ }
+ }
+
+ private final Provider<CurrentUser> self;
+ private final Provider<ReviewDb> dbProvider;
+ private final AccountCache accountCache;
+
+ @Inject
+ PutHttpPassword(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider,
+ AccountCache accountCache) {
+ this.self = self;
+ this.dbProvider = dbProvider;
+ this.accountCache = accountCache;
+ }
+
+ @Override
+ public Response<String> apply(AccountResource rsrc, Input input) throws AuthException,
+ ResourceNotFoundException, ResourceConflictException, OrmException {
+ if (input == null) {
+ input = new Input();
+ }
+ input.httpPassword = Strings.emptyToNull(input.httpPassword);
+
+ String newPassword;
+ if (input.generate) {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canGenerateHttpPassword()) {
+ throw new AuthException("not allowed to generate HTTP password");
+ }
+ newPassword = generate();
+
+ } else if (input.httpPassword == null) {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("not allowed to clear HTTP password");
+ }
+ newPassword = null;
+ } else {
+ if (!self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("not allowed to set HTTP password directly, "
+ + "need to be Gerrit administrator");
+ }
+ newPassword = input.httpPassword;
+ }
+
+ if (rsrc.getUser().getUserName() == null) {
+ throw new ResourceConflictException("username must be set");
+ }
+
+ AccountExternalId id = dbProvider.get().accountExternalIds()
+ .get(new AccountExternalId.Key(
+ SCHEME_USERNAME,
+ rsrc.getUser().getUserName()));
+ if (id == null) {
+ throw new ResourceNotFoundException();
+ }
+ id.setPassword(newPassword);
+ dbProvider.get().accountExternalIds().update(Collections.singleton(id));
+ accountCache.evict(rsrc.getUser().getAccountId());
+
+ return Strings.isNullOrEmpty(newPassword)
+ ? Response.<String>none()
+ : Response.ok(newPassword);
+ }
+
+ private static String generate() {
+ byte[] rand = new byte[LEN];
+ rng.nextBytes(rand);
+
+ byte[] enc = Base64.encodeBase64(rand, false);
+ StringBuilder r = new StringBuilder(LEN);
+ for (int i = 0; i < LEN; i++) {
+ if (enc[i] == '=') {
+ break;
+ }
+ r.append((char) enc[i]);
+ }
+ return r.toString();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
new file mode 100644
index 0000000..50497c7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.PutName.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+public class PutName implements RestModifyView<AccountResource, Input> {
+ public static class Input {
+ @DefaultInput
+ public String name;
+ }
+
+ private final Provider<CurrentUser> self;
+ private final Realm realm;
+ private final Provider<ReviewDb> dbProvider;
+ private final AccountCache byIdCache;
+
+ @Inject
+ PutName(Provider<CurrentUser> self, Realm realm,
+ Provider<ReviewDb> dbProvider, AccountCache byIdCache) {
+ this.self = self;
+ this.realm = realm;
+ this.dbProvider = dbProvider;
+ this.byIdCache = byIdCache;
+ }
+
+ @Override
+ public Response<String> apply(AccountResource rsrc, Input input)
+ throws AuthException, MethodNotAllowedException,
+ ResourceNotFoundException, OrmException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("not allowed to change name");
+ }
+
+ if (!realm.allowsEdit(FieldName.FULL_NAME)) {
+ throw new MethodNotAllowedException("realm does not allow editing name");
+ }
+
+ if (input == null) {
+ input = new Input();
+ }
+
+ Account a = dbProvider.get().accounts().get(rsrc.getUser().getAccountId());
+ if (a == null) {
+ throw new ResourceNotFoundException("account not found");
+ }
+ a.setFullName(input.name);
+ dbProvider.get().accounts().update(Collections.singleton(a));
+ byIdCache.evict(a.getId());
+ return Strings.isNullOrEmpty(a.getFullName())
+ ? Response.<String> none()
+ : Response.ok(a.getFullName());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
new file mode 100644
index 0000000..d81f361
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.PutPreferred.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+public class PutPreferred implements
+ RestModifyView<AccountResource.Email, Input> {
+ static class Input {
+ }
+
+ private final Provider<CurrentUser> self;
+ private final Provider<ReviewDb> dbProvider;
+ private final AccountCache byIdCache;
+
+ @Inject
+ PutPreferred(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider,
+ AccountCache byIdCache) {
+ this.self = self;
+ this.dbProvider = dbProvider;
+ this.byIdCache = byIdCache;
+ }
+
+ @Override
+ public Response<String> apply(AccountResource.Email rsrc, Input input)
+ throws AuthException, ResourceNotFoundException, OrmException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("not allowed to set preferred email address");
+ }
+
+ Account a = dbProvider.get().accounts().get(rsrc.getUser().getAccountId());
+ if (a == null) {
+ throw new ResourceNotFoundException("account not found");
+ }
+ if (rsrc.getEmail().equals(a.getPreferredEmail())) {
+ return Response.ok("");
+ }
+ a.setPreferredEmail(rsrc.getEmail());
+ dbProvider.get().accounts().update(Collections.singleton(a));
+ byIdCache.evict(a.getId());
+ return Response.created("");
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
new file mode 100644
index 0000000..8e40b2e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -0,0 +1,139 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.TimeFormat;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.SetPreferences.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+public class SetPreferences implements RestModifyView<AccountResource, Input> {
+ static class Input {
+ Short changesPerPage;
+ Boolean showSiteHeader;
+ Boolean useFlashClipboard;
+ DownloadScheme downloadScheme;
+ DownloadCommand downloadCommand;
+ Boolean copySelfOnEmail;
+ DateFormat dateFormat;
+ TimeFormat timeFormat;
+ Boolean reversePatchSetOrder;
+ Boolean showUsernameInReviewCategory;
+ Boolean relativeDateInChangeTable;
+ CommentVisibilityStrategy commentVisibilityStrategy;
+ DiffView diffView;
+ }
+
+ private final Provider<CurrentUser> self;
+ private final AccountCache cache;
+ private final ReviewDb db;
+
+ @Inject
+ SetPreferences(Provider<CurrentUser> self, AccountCache cache, ReviewDb db) {
+ this.self = self;
+ this.cache = cache;
+ this.db = db;
+ }
+
+ @Override
+ public GetPreferences.PreferenceInfo apply(AccountResource rsrc, Input i)
+ throws AuthException, ResourceNotFoundException, OrmException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new AuthException("restricted to administrator");
+ }
+ if (i == null) {
+ i = new Input();
+ }
+
+ Account.Id accountId = rsrc.getUser().getAccountId();
+ AccountGeneralPreferences p;
+ db.accounts().beginTransaction(accountId);
+ try {
+ Account a = db.accounts().get(accountId);
+ if (a == null) {
+ throw new ResourceNotFoundException();
+ }
+
+ p = a.getGeneralPreferences();
+ if (p == null) {
+ p = new AccountGeneralPreferences();
+ a.setGeneralPreferences(p);
+ }
+
+ if (i.changesPerPage != null) {
+ p.setMaximumPageSize(i.changesPerPage);
+ }
+ if (i.showSiteHeader != null) {
+ p.setShowSiteHeader(i.showSiteHeader);
+ }
+ if (i.useFlashClipboard != null) {
+ p.setUseFlashClipboard(i.useFlashClipboard);
+ }
+ if (i.downloadScheme != null) {
+ p.setDownloadUrl(i.downloadScheme);
+ }
+ if (i.downloadCommand != null) {
+ p.setDownloadCommand(i.downloadCommand);
+ }
+ if (i.copySelfOnEmail != null) {
+ p.setCopySelfOnEmails(i.copySelfOnEmail);
+ }
+ if (i.dateFormat != null) {
+ p.setDateFormat(i.dateFormat);
+ }
+ if (i.timeFormat != null) {
+ p.setTimeFormat(i.timeFormat);
+ }
+ if (i.reversePatchSetOrder != null) {
+ p.setReversePatchSetOrder(i.reversePatchSetOrder);
+ }
+ if (i.showUsernameInReviewCategory != null) {
+ p.setShowUsernameInReviewCategory(i.showUsernameInReviewCategory);
+ }
+ if (i.relativeDateInChangeTable != null) {
+ p.setRelativeDateInChangeTable(i.relativeDateInChangeTable);
+ }
+ if (i.commentVisibilityStrategy != null) {
+ p.setCommentVisibilityStrategy(i.commentVisibilityStrategy);
+ }
+ if (i.diffView != null) {
+ p.setDiffView(i.diffView);
+ }
+
+ db.accounts().update(Collections.singleton(a));
+ db.commit();
+ cache.evict(accountId);
+ } finally {
+ db.rollback();
+ }
+ return new GetPreferences.PreferenceInfo(p);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
new file mode 100644
index 0000000..5578c3f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class SshKeys implements
+ ChildCollection<AccountResource, AccountResource.SshKey> {
+ private final DynamicMap<RestView<AccountResource.SshKey>> views;
+ private final Provider<GetSshKeys> list;
+ private final Provider<CurrentUser> self;
+ private final Provider<ReviewDb> dbProvider;
+
+ @Inject
+ SshKeys(DynamicMap<RestView<AccountResource.SshKey>> views,
+ Provider<GetSshKeys> list, Provider<CurrentUser> self,
+ Provider<ReviewDb> dbProvider) {
+ this.views = views;
+ this.list = list;
+ this.self = self;
+ this.dbProvider = dbProvider;
+ }
+
+ @Override
+ public RestView<AccountResource> list() {
+ return list.get();
+ }
+
+ @Override
+ public AccountResource.SshKey parse(AccountResource rsrc, IdString id)
+ throws ResourceNotFoundException, OrmException {
+ if (self.get() != rsrc.getUser()
+ && !self.get().getCapabilities().canAdministrateServer()) {
+ throw new ResourceNotFoundException();
+ }
+
+ try {
+ int seq = Integer.parseInt(id.get(), 10);
+ AccountSshKey sshKey =
+ dbProvider.get().accountSshKeys()
+ .get(new AccountSshKey.Id(rsrc.getUser().getAccountId(), seq));
+ if (sshKey == null) {
+ throw new ResourceNotFoundException(id);
+ }
+ return new AccountResource.SshKey(rsrc.getUser(), sshKey);
+ } catch (NumberFormatException e) {
+ throw new ResourceNotFoundException(id);
+ }
+ }
+
+ @Override
+ public DynamicMap<RestView<AccountResource.SshKey>> views() {
+ return views;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/actions/ActionInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/actions/ActionInfo.java
new file mode 100644
index 0000000..91574a2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/actions/ActionInfo.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.actions;
+
+import com.google.gerrit.extensions.webui.UiAction;
+
+public class ActionInfo {
+ String method;
+ String label;
+ String title;
+ Boolean enabled;
+
+ public ActionInfo(UiAction.Description d) {
+ method = d.getMethod();
+ label = d.getLabel();
+ title = d.getTitle();
+ enabled = d.isEnabled() ? true : null;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
index 7b44c02..8771c23d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
@@ -16,9 +16,11 @@
import com.google.gerrit.common.ProjectUtil;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import org.kohsuke.args4j.CmdLineException;
@@ -27,17 +29,26 @@
import org.kohsuke.args4j.spi.OptionHandler;
import org.kohsuke.args4j.spi.Parameters;
import org.kohsuke.args4j.spi.Setter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
public class ProjectControlHandler extends OptionHandler<ProjectControl> {
- private final ProjectControl.Factory projectControlFactory;
+ private static final Logger log = LoggerFactory
+ .getLogger(ProjectControlHandler.class);
+ private final ProjectControl.GenericFactory projectControlFactory;
+ private final Provider<CurrentUser> user;
@Inject
public ProjectControlHandler(
- final ProjectControl.Factory projectControlFactory,
+ final ProjectControl.GenericFactory projectControlFactory,
+ Provider<CurrentUser> user,
@Assisted final CmdLineParser parser, @Assisted final OptionDef option,
@Assisted final Setter<ProjectControl> setter) {
super(parser, option, setter);
this.projectControlFactory = projectControlFactory;
+ this.user = user;
}
@Override
@@ -59,13 +70,21 @@
}
String nameWithoutSuffix = ProjectUtil.stripGitSuffix(projectName);
+ Project.NameKey nameKey = new Project.NameKey(nameWithoutSuffix);
final ProjectControl control;
try {
- Project.NameKey nameKey = new Project.NameKey(nameWithoutSuffix);
- control = projectControlFactory.validateFor(nameKey, ProjectControl.OWNER | ProjectControl.VISIBLE);
+ control = projectControlFactory.validateFor(
+ nameKey,
+ ProjectControl.OWNER | ProjectControl.VISIBLE,
+ user.get());
} catch (NoSuchProjectException e) {
throw new CmdLineException(owner, e.getMessage());
+ } catch (IOException e) {
+ log.warn("Cannot load project " + nameWithoutSuffix, e);
+ throw new CmdLineException(
+ owner,
+ new NoSuchProjectException(nameKey).getMessage());
}
setter.addValue(control);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java
index 7d08173..6725745 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java
@@ -26,7 +26,7 @@
/**
* Globally unique identifier for the user.
*/
- public final static class UUID {
+ public static final class UUID {
private final String uuid;
/**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
index 97a0309..aedd3e6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -126,7 +126,7 @@
String groupDn = uuid.get().substring(LDAP_UUID.length());
CurrentUser user = userProvider.get();
- if (!(user instanceof IdentifiedUser)
+ if (!(user.isIdentifiedUser())
|| !membershipsOf((IdentifiedUser) user).contains(uuid)) {
try {
if (!existsCache.get(groupDn)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index f766297..6b4c0e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -21,6 +21,7 @@
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -28,6 +29,7 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.Abandon.Input;
+import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.mail.AbandonedSender;
import com.google.gerrit.server.mail.ReplyToChangeSender;
import com.google.gerrit.server.project.ChangeControl;
@@ -41,13 +43,15 @@
import java.util.Collections;
-public class Abandon implements RestModifyView<ChangeResource, Input> {
+public class Abandon implements RestModifyView<ChangeResource, Input>,
+ UiAction<ChangeResource> {
private static final Logger log = LoggerFactory.getLogger(Abandon.class);
private final ChangeHooks hooks;
private final AbandonedSender.Factory abandonedSenderFactory;
private final Provider<ReviewDb> dbProvider;
private final ChangeJson json;
+ private final ChangeIndexer indexer;
public static class Input {
@DefaultInput
@@ -58,11 +62,13 @@
Abandon(ChangeHooks hooks,
AbandonedSender.Factory abandonedSenderFactory,
Provider<ReviewDb> dbProvider,
- ChangeJson json) {
+ ChangeJson json,
+ ChangeIndexer indexer) {
this.hooks = hooks;
this.abandonedSenderFactory = abandonedSenderFactory;
this.dbProvider = dbProvider;
this.json = json;
+ this.indexer = indexer;
}
@Override
@@ -107,6 +113,7 @@
db.rollback();
}
+ indexer.index(change);
try {
ReplyToChangeSender cm = abandonedSenderFactory.create(change);
cm.setFrom(caller.getAccountId());
@@ -117,11 +124,20 @@
}
hooks.doChangeAbandonedHook(change,
caller.getAccount(),
+ db.patchSets().get(change.currentPatchSetId()),
Strings.emptyToNull(input.message),
db);
return json.format(change);
}
+ @Override
+ public UiAction.Description getDescription(ChangeResource resource) {
+ return new UiAction.Description()
+ .setLabel("Abandon")
+ .setVisible(resource.getChange().getStatus().isOpen()
+ && resource.getControl().canAbandon());
+ }
+
private ChangeMessage newMessage(Input input, IdentifiedUser caller,
Change change) throws OrmException {
StringBuilder msg = new StringBuilder();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 5c965e2..0d051a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.change;
+import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
+
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.reviewdb.client.Account;
@@ -21,65 +23,172 @@
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.RefControl;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Set;
public class ChangeInserter {
+ public static interface Factory {
+ ChangeInserter create(RefControl ctl, Change c, RevCommit rc);
+ }
+
+ private static final Logger log =
+ LoggerFactory.getLogger(ChangeInserter.class);
+
+ private final Provider<ReviewDb> dbProvider;
private final GitReferenceUpdated gitRefUpdated;
private final ChangeHooks hooks;
private final ApprovalsUtil approvalsUtil;
private final TrackingFooters trackingFooters;
+ private final ChangeIndexer indexer;
+ private final CreateChangeSender.Factory createChangeSenderFactory;
+
+ private final RefControl refControl;
+ private final Change change;
+ private final PatchSet patchSet;
+ private final RevCommit commit;
+ private final PatchSetInfo patchSetInfo;
+
+ private ChangeMessage changeMessage;
+ private Set<Account.Id> reviewers;
+ private Set<Account.Id> extraCC;
+ private boolean runHooks;
+ private boolean sendMail;
@Inject
- public ChangeInserter(final GitReferenceUpdated gitRefUpdated,
- ChangeHooks hooks, ApprovalsUtil approvalsUtil,
- TrackingFooters trackingFooters) {
+ ChangeInserter(Provider<ReviewDb> dbProvider,
+ PatchSetInfoFactory patchSetInfoFactory,
+ GitReferenceUpdated gitRefUpdated,
+ ChangeHooks hooks,
+ ApprovalsUtil approvalsUtil,
+ TrackingFooters trackingFooters,
+ ChangeIndexer indexer,
+ CreateChangeSender.Factory createChangeSenderFactory,
+ @Assisted RefControl refControl,
+ @Assisted Change change,
+ @Assisted RevCommit commit) {
+ this.dbProvider = dbProvider;
this.gitRefUpdated = gitRefUpdated;
this.hooks = hooks;
this.approvalsUtil = approvalsUtil;
this.trackingFooters = trackingFooters;
+ this.indexer = indexer;
+ this.createChangeSenderFactory = createChangeSenderFactory;
+ this.refControl = refControl;
+ this.change = change;
+ this.commit = commit;
+ this.reviewers = Collections.emptySet();
+ this.extraCC = Collections.emptySet();
+ this.runHooks = true;
+ this.sendMail = true;
+
+ patchSet =
+ new PatchSet(new PatchSet.Id(change.getId(), INITIAL_PATCH_SET_ID));
+ patchSet.setCreatedOn(change.getCreatedOn());
+ patchSet.setUploader(change.getOwner());
+ patchSet.setRevision(new RevId(commit.name()));
+ patchSetInfo = patchSetInfoFactory.get(commit, patchSet.getId());
+ change.setCurrentPatchSet(patchSetInfo);
+ ChangeUtil.computeSortKey(change);
}
- public void insertChange(ReviewDb db, Change change, PatchSet ps,
- RevCommit commit, LabelTypes labelTypes, PatchSetInfo info,
- Set<Account.Id> reviewers) throws OrmException {
- insertChange(db, change, null, ps, commit, labelTypes, info, reviewers);
+ public ChangeInserter setMessage(ChangeMessage changeMessage) {
+ this.changeMessage = changeMessage;
+ return this;
}
- public void insertChange(ReviewDb db, Change change,
- ChangeMessage changeMessage, PatchSet ps, RevCommit commit,
- LabelTypes labelTypes, PatchSetInfo info, Set<Account.Id> reviewers)
- throws OrmException {
+ public ChangeInserter setReviewers(Set<Account.Id> reviewers) {
+ this.reviewers = reviewers;
+ return this;
+ }
+ public ChangeInserter setExtraCC(Set<Account.Id> extraCC) {
+ this.extraCC = extraCC;
+ return this;
+ }
+
+ public ChangeInserter setDraft(boolean draft) {
+ change.setStatus(draft ? Change.Status.DRAFT : Change.Status.NEW);
+ patchSet.setDraft(draft);
+ return this;
+ }
+
+ public ChangeInserter setRunHooks(boolean runHooks) {
+ this.runHooks = runHooks;
+ return this;
+ }
+
+ public ChangeInserter setSendMail(boolean sendMail) {
+ this.sendMail = sendMail;
+ return this;
+ }
+
+ public PatchSet getPatchSet() {
+ return patchSet;
+ }
+
+ public PatchSetInfo getPatchSetInfo() {
+ return patchSetInfo;
+ }
+
+ public void insert() throws OrmException {
+ ReviewDb db = dbProvider.get();
db.changes().beginTransaction(change.getId());
try {
- ChangeUtil.insertAncestors(db, ps.getId(), commit);
- db.patchSets().insert(Collections.singleton(ps));
+ ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
+ db.patchSets().insert(Collections.singleton(patchSet));
db.changes().insert(Collections.singleton(change));
ChangeUtil.updateTrackingIds(db, change, trackingFooters, commit.getFooterLines());
- approvalsUtil.addReviewers(db, labelTypes, change, ps, info, reviewers,
- Collections.<Account.Id> emptySet());
- if (changeMessage != null) {
- db.changeMessages().insert(Collections.singleton(changeMessage));
- }
+ LabelTypes labelTypes = refControl.getProjectControl().getLabelTypes();
+ approvalsUtil.addReviewers(db, labelTypes, change, patchSet, patchSetInfo,
+ reviewers, Collections.<Account.Id> emptySet());
db.commit();
} finally {
db.rollback();
}
+ if (changeMessage != null) {
+ db.changeMessages().insert(Collections.singleton(changeMessage));
+ }
- gitRefUpdated.fire(change.getProject(), ps.getRefName(), ObjectId.zeroId(),
- commit);
- hooks.doPatchsetCreatedHook(change, ps, db);
+ indexer.index(change);
+ gitRefUpdated.fire(change.getProject(), patchSet.getRefName(),
+ ObjectId.zeroId(), commit);
+
+ if (runHooks) {
+ hooks.doPatchsetCreatedHook(change, patchSet, db);
+ }
+
+ if (sendMail) {
+ try {
+ CreateChangeSender cm =
+ createChangeSenderFactory.create(change);
+ cm.setFrom(change.getOwner());
+ cm.setPatchSet(patchSet, patchSetInfo);
+ cm.addReviewers(reviewers);
+ cm.addExtraCC(extraCC);
+ cm.send();
+ } catch (Exception err) {
+ log.error("Cannot send email for new change " + change.getId(), err);
+ }
+ }
}
}
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 8c8ecfc..8f16bb4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.common.changes.ListChangesOption.ALL_COMMITS;
import static com.google.gerrit.common.changes.ListChangesOption.ALL_FILES;
import static com.google.gerrit.common.changes.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_ACTIONS;
import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_COMMIT;
import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_FILES;
import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_REVISION;
@@ -24,6 +25,7 @@
import static com.google.gerrit.common.changes.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.common.changes.ListChangesOption.LABELS;
import static com.google.gerrit.common.changes.ListChangesOption.MESSAGES;
+import static com.google.gerrit.common.changes.ListChangesOption.REVIEWED;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
@@ -31,6 +33,7 @@
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
@@ -45,10 +48,12 @@
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -61,26 +66,24 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.actions.ActionInfo;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.extensions.webui.UiActions;
import com.google.gerrit.server.git.LabelNormalizer;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListEntry;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.ssh.SshAdvertisedAddresses;
import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import com.jcraft.jsch.HostKey;
-
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -90,6 +93,7 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -97,16 +101,37 @@
public class ChangeJson {
private static final Logger log = LoggerFactory.getLogger(ChangeJson.class);
+ private static final ResultSet<ChangeMessage> NO_MESSAGES =
+ new ResultSet<ChangeMessage>() {
+ @Override
+ public Iterator<ChangeMessage> iterator() {
+ return toList().iterator();
+ }
+
+ @Override
+ public List<ChangeMessage> toList() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void close() {
+ }
+ };
@Singleton
static class Urls {
final String git;
final String http;
+ final String ssh;
@Inject
- Urls(@GerritServerConfig Config cfg) {
+ Urls(@GerritServerConfig Config cfg,
+ @SshAdvertisedAddresses List<String> sshAddresses) {
this.git = ensureSlash(cfg.getString("gerrit", null, "canonicalGitUrl"));
this.http = ensureSlash(cfg.getString("gerrit", null, "gitHttpUrl"));
+ this.ssh = !sshAddresses.isEmpty()
+ ? ensureSlash("ssh://" + sshAddresses.get(0))
+ : null;
}
private static String ensureSlash(String in) {
@@ -119,45 +144,52 @@
private final Provider<ReviewDb> db;
private final LabelNormalizer labelNormalizer;
- private final CurrentUser user;
+ private final Provider<CurrentUser> userProvider;
private final AnonymousUser anonymous;
private final IdentifiedUser.GenericFactory userFactory;
private final ChangeControl.GenericFactory changeControlGenericFactory;
private final PatchSetInfoFactory patchSetInfoFactory;
- private final PatchListCache patchListCache;
+ private final FileInfoJson fileInfoJson;
private final AccountInfo.Loader.Factory accountLoaderFactory;
private final Provider<String> urlProvider;
private final Urls urls;
+ private final DynamicMap<RestView<ChangeResource>> changes;
+ private final Revisions revisions;
+
private ChangeControl.Factory changeControlUserFactory;
- private SshInfo sshInfo;
private EnumSet<ListChangesOption> options;
private AccountInfo.Loader accountLoader;
private ChangeControl lastControl;
+ private Set<Change.Id> reviewed;
@Inject
ChangeJson(
Provider<ReviewDb> db,
LabelNormalizer ln,
- CurrentUser u,
+ Provider<CurrentUser> userProvider,
AnonymousUser au,
IdentifiedUser.GenericFactory uf,
ChangeControl.GenericFactory ccf,
PatchSetInfoFactory psi,
- PatchListCache plc,
+ FileInfoJson fileInfoJson,
AccountInfo.Loader.Factory ailf,
@CanonicalWebUrl Provider<String> curl,
- Urls urls) {
+ Urls urls,
+ DynamicMap<RestView<ChangeResource>> changes,
+ Revisions revisions) {
this.db = db;
this.labelNormalizer = ln;
- this.user = u;
+ this.userProvider = userProvider;
this.anonymous = au;
this.userFactory = uf;
this.changeControlGenericFactory = ccf;
this.patchSetInfoFactory = psi;
- this.patchListCache = plc;
+ this.fileInfoJson = fileInfoJson;
this.accountLoaderFactory = ailf;
this.urlProvider = curl;
this.urls = urls;
+ this.changes = changes;
+ this.revisions = revisions;
options = EnumSet.noneOf(ListChangesOption.class);
}
@@ -172,11 +204,6 @@
return this;
}
- public ChangeJson setSshInfo(SshInfo info) {
- sshInfo = info;
- return this;
- }
-
public ChangeJson setChangeControlFactory(ChangeControl.Factory cf) {
changeControlUserFactory = cf;
return this;
@@ -208,12 +235,22 @@
public List<List<ChangeInfo>> formatList2(List<List<ChangeData>> in)
throws OrmException {
accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+ Iterable<ChangeData> all = Iterables.concat(in);
+ ChangeData.ensureChangeLoaded(db, all);
+ if (has(ALL_REVISIONS)) {
+ ChangeData.ensureAllPatchSetsLoaded(db, all);
+ } else {
+ ChangeData.ensureCurrentPatchSetLoaded(db, all);
+ }
+ if (has(REVIEWED)) {
+ ensureReviewedLoaded(all);
+ }
+ ChangeData.ensureCurrentApprovalsLoaded(db, all);
+
List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
+ Map<Change.Id, ChangeInfo> out = Maps.newHashMap();
for (List<ChangeData> changes : in) {
- ChangeData.ensureChangeLoaded(db, changes);
- ChangeData.ensureCurrentPatchSetLoaded(db, changes);
- ChangeData.ensureCurrentApprovalsLoaded(db, changes);
- res.add(toChangeInfo(changes));
+ res.add(toChangeInfo(out, changes));
}
accountLoader.fill();
return res;
@@ -223,11 +260,16 @@
return options.contains(option);
}
- private List<ChangeInfo> toChangeInfo(List<ChangeData> changes)
- throws OrmException {
+ private List<ChangeInfo> toChangeInfo(Map<Change.Id, ChangeInfo> out,
+ List<ChangeData> changes) throws OrmException {
List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
for (ChangeData cd : changes) {
- info.add(toChangeInfo(cd));
+ ChangeInfo i = out.get(cd.getId());
+ if (i == null) {
+ i = toChangeInfo(cd);
+ out.put(cd.getId(), i);
+ }
+ info.add(i);
}
return info;
}
@@ -247,8 +289,12 @@
out.updated = in.getLastUpdatedOn();
out._number = in.getId().get();
out._sortkey = in.getSortKey();
- out.starred = user.getStarredChanges().contains(in.getId()) ? true : null;
- out.reviewed = in.getStatus().isOpen() && isChangeReviewed(cd) ? true : null;
+ out.starred = userProvider.get().getStarredChanges().contains(in.getId())
+ ? true
+ : null;
+ out.reviewed = in.getStatus().isOpen()
+ && has(REVIEWED)
+ && reviewed.contains(cd.getId()) ? true : null;
out.labels = labelsFor(cd, has(LABELS), has(DETAILED_LABELS));
Collection<PatchSet.Id> limited = cd.getLimitedPatchSets();
@@ -277,13 +323,22 @@
}
}
+ if (has(CURRENT_ACTIONS) && userProvider.get().isIdentifiedUser()) {
+ out.actions = Maps.newTreeMap();
+ for (UiAction.Description d : UiActions.from(
+ changes,
+ new ChangeResource(control(cd)),
+ userProvider)) {
+ out.actions.put(d.getId(), new ActionInfo(d));
+ }
+ }
lastControl = null;
return out;
}
private ChangeControl control(ChangeData cd) throws OrmException {
ChangeControl ctrl = cd.changeControl();
- if (ctrl != null && ctrl.getCurrentUser() == user) {
+ if (ctrl != null && ctrl.getCurrentUser() == userProvider.get()) {
return ctrl;
} else if (lastControl != null
&& cd.getId().equals(lastControl.getChange().getId())) {
@@ -294,7 +349,8 @@
if (changeControlUserFactory != null) {
ctrl = changeControlUserFactory.controlFor(cd.change(db));
} else {
- ctrl = changeControlGenericFactory.controlFor(cd.change(db), user);
+ ctrl = changeControlGenericFactory.controlFor(cd.change(db),
+ userProvider.get());
}
} catch (NoSuchChangeException e) {
return null;
@@ -330,11 +386,6 @@
return null;
}
- PatchSet ps = cd.currentPatchSet(db);
- if (ps == null) {
- return null;
- }
-
LabelTypes labelTypes = ctl.getLabelTypes();
if (cd.getChange().getStatus().isOpen()) {
return labelsForOpenChange(cd, labelTypes, standard, detailed);
@@ -471,16 +522,18 @@
continue;
}
Integer value;
+ Timestamp date = null;
PatchSetApproval psa = current.get(accountId, lt.getName());
if (psa != null) {
value = Integer.valueOf(psa.getValue());
+ date = psa.getGranted();
} else {
// Either the user cannot vote on this label, or there just wasn't a
// dummy approval for this label. Explicitly check whether the user
// can vote on this label.
value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null;
}
- e.getValue().addApproval(approvalInfo(accountId, value));
+ e.getValue().addApproval(approvalInfo(accountId, value, date));
}
}
}
@@ -524,7 +577,7 @@
if (detailed) {
for (String name : labels.keySet()) {
- ApprovalInfo ai = approvalInfo(accountId, 0);
+ ApprovalInfo ai = approvalInfo(accountId, 0, null);
byLabel.put(name, ai);
labels.get(name).addApproval(ai);
}
@@ -539,6 +592,7 @@
ApprovalInfo info = byLabel.get(type.getName());
if (info != null) {
info.value = Integer.valueOf(val);
+ info.date = psa.getGranted();
}
LabelInfo li = labels.get(type.getName());
@@ -552,9 +606,10 @@
return labels;
}
- private ApprovalInfo approvalInfo(Account.Id id, Integer value) {
+ private ApprovalInfo approvalInfo(Account.Id id, Integer value, Timestamp date) {
ApprovalInfo ai = new ApprovalInfo(id);
ai.value = value;
+ ai.date = date;
accountLoader.put(ai);
return ai;
}
@@ -675,38 +730,49 @@
return result;
}
- private boolean isChangeReviewed(ChangeData cd) throws OrmException {
- if (user instanceof IdentifiedUser) {
- PatchSet currentPatchSet = cd.currentPatchSet(db);
- if (currentPatchSet == null) {
- return false;
- }
-
- List<ChangeMessage> messages =
- db.get().changeMessages().byPatchSet(currentPatchSet.getId()).toList();
-
- if (messages.isEmpty()) {
- return false;
- }
-
- // Sort messages to let the most recent ones at the beginning.
- Collections.sort(messages, new Comparator<ChangeMessage>() {
- @Override
- public int compare(ChangeMessage a, ChangeMessage b) {
- return b.getWrittenOn().compareTo(a.getWrittenOn());
+ private void ensureReviewedLoaded(Iterable<ChangeData> all)
+ throws OrmException {
+ reviewed = Sets.newHashSet();
+ if (userProvider.get().isIdentifiedUser()) {
+ Account.Id self = ((IdentifiedUser) userProvider.get()).getAccountId();
+ for (List<ChangeData> batch : Iterables.partition(all, 50)) {
+ List<ResultSet<ChangeMessage>> m =
+ Lists.newArrayListWithCapacity(batch.size());
+ for (ChangeData cd : batch) {
+ PatchSet.Id ps = cd.change(db).currentPatchSetId();
+ if (ps != null && cd.change(db).getStatus().isOpen()) {
+ m.add(db.get().changeMessages().byPatchSet(ps));
+ } else {
+ m.add(NO_MESSAGES);
+ }
}
- });
-
- Account.Id currentUserId = ((IdentifiedUser) user).getAccountId();
- Account.Id changeOwnerId = cd.change(db).getOwner();
- for (ChangeMessage cm : messages) {
- if (currentUserId.equals(cm.getAuthor())) {
- return true;
- } else if (changeOwnerId.equals(cm.getAuthor())) {
- return false;
+ for (int i = 0; i < m.size(); i++) {
+ if (isChangeReviewed(self, batch.get(i), m.get(i).toList())) {
+ reviewed.add(batch.get(i).getId());
+ }
}
}
}
+ }
+
+ private boolean isChangeReviewed(Account.Id self, ChangeData cd,
+ List<ChangeMessage> msgs) throws OrmException {
+ // Sort messages to keep the most recent ones at the beginning.
+ Collections.sort(msgs, new Comparator<ChangeMessage>() {
+ @Override
+ public int compare(ChangeMessage a, ChangeMessage b) {
+ return b.getWrittenOn().compareTo(a.getWrittenOn());
+ }
+ });
+
+ Account.Id changeOwnerId = cd.change(db).getOwner();
+ for (ChangeMessage cm : msgs) {
+ if (self.equals(cm.getAuthor())) {
+ return true;
+ } else if (changeOwnerId.equals(cm.getAuthor())) {
+ return false;
+ }
+ }
return false;
}
@@ -741,74 +807,53 @@
if (has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT))) {
try {
- PatchSetInfo info = patchSetInfoFactory.get(db.get(), in.getId());
- out.commit = new CommitInfo();
- out.commit.parents = Lists.newArrayListWithCapacity(info.getParents().size());
- out.commit.author = toGitPerson(info.getAuthor());
- out.commit.committer = toGitPerson(info.getCommitter());
- out.commit.subject = info.getSubject();
- out.commit.message = info.getMessage();
-
- for (ParentInfo parent : info.getParents()) {
- CommitInfo i = new CommitInfo();
- i.commit = parent.id.get();
- i.subject = parent.shortMessage;
- out.commit.parents.add(i);
- }
+ out.commit = toCommit(in);
} catch (PatchSetInfoNotAvailableException e) {
log.warn("Cannot load PatchSetInfo " + in.getId(), e);
}
}
if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) {
- PatchList list;
try {
- list = patchListCache.get(cd.change(db), in);
+ out.files = fileInfoJson.toFileInfoMap(cd.change(db), in);
+ out.files.remove(Patch.COMMIT_MSG);
} catch (PatchListNotAvailableException e) {
log.warn("Cannot load PatchList " + in.getId(), e);
- list = null;
}
- if (list != null) {
- out.files = Maps.newTreeMap();
- for (PatchListEntry e : list.getPatches()) {
- if (Patch.COMMIT_MSG.equals(e.getNewName())) {
- continue;
- }
+ }
- FileInfo d = new FileInfo();
- d.status = e.getChangeType() != Patch.ChangeType.MODIFIED
- ? e.getChangeType().getCode()
- : null;
- d.oldPath = e.getOldName();
- if (e.getPatchType() == Patch.PatchType.BINARY) {
- d.binary = true;
- } else {
- d.linesInserted = e.getInsertions() > 0 ? e.getInsertions() : null;
- d.linesDeleted = e.getDeletions() > 0 ? e.getDeletions() : null;
- }
-
- FileInfo o = out.files.put(e.getNewName(), d);
- if (o != null) {
- // This should only happen on a delete-add break created by JGit
- // when the file was rewritten and too little content survived. Write
- // a single record with data from both sides.
- d.status = Patch.ChangeType.REWRITE.getCode();
- if (o.binary != null && o.binary) {
- d.binary = true;
- }
- if (o.linesInserted != null) {
- d.linesInserted = o.linesInserted;
- }
- if (o.linesDeleted != null) {
- d.linesDeleted = o.linesDeleted;
- }
- }
- }
+ if (out.isCurrent && has(CURRENT_ACTIONS)
+ && userProvider.get().isIdentifiedUser()) {
+ out.actions = Maps.newTreeMap();
+ for (UiAction.Description d : UiActions.from(
+ revisions,
+ new RevisionResource(new ChangeResource(control(cd)), in),
+ userProvider)) {
+ out.actions.put(d.getId(), new ActionInfo(d));
}
}
return out;
}
+ CommitInfo toCommit(PatchSet in)
+ throws PatchSetInfoNotAvailableException {
+ PatchSetInfo info = patchSetInfoFactory.get(db.get(), in.getId());
+ CommitInfo commit = new CommitInfo();
+ commit.parents = Lists.newArrayListWithCapacity(info.getParents().size());
+ commit.author = toGitPerson(info.getAuthor());
+ commit.committer = toGitPerson(info.getCommitter());
+ commit.subject = info.getSubject();
+ commit.message = info.getMessage();
+
+ for (ParentInfo parent : info.getParents()) {
+ CommitInfo i = new CommitInfo();
+ i.commit = parent.id.get();
+ i.subject = parent.shortMessage;
+ commit.parents.add(i);
+ }
+ return commit;
+ }
+
private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
throws OrmException {
Map<String, FetchInfo> r = Maps.newLinkedHashMap();
@@ -830,11 +875,9 @@
+ cd.change(db).getProject().get(), refName));
}
}
- if (sshInfo != null && !sshInfo.getHostKeys().isEmpty()) {
- HostKey host = sshInfo.getHostKeys().get(0);
- r.put("ssh", new FetchInfo(String.format(
- "ssh://%s/%s",
- host.getHost(), cd.change(db).getProject().get()),
+ if (urls.ssh != null) {
+ r.put("ssh", new FetchInfo(
+ urls.ssh + cd.change(db).getProject().get(),
refName));
}
@@ -870,6 +913,7 @@
AccountInfo owner;
+ Map<String, ActionInfo> actions;
Map<String, LabelInfo> labels;
Map<String, Collection<String>> permitted_labels;
Collection<AccountInfo> removable_reviewers;
@@ -893,7 +937,8 @@
int _number;
Map<String, FetchInfo> fetch;
CommitInfo commit;
- Map<String, FileInfo> files;
+ Map<String, FileInfoJson.FileInfo> files;
+ Map<String, ActionInfo> actions;
}
static class FetchInfo {
@@ -914,6 +959,7 @@
}
static class CommitInfo {
+ final String kind = "gerritcodereview#commit";
String commit;
List<CommitInfo> parents;
GitPerson author;
@@ -922,14 +968,6 @@
String message;
}
- static class FileInfo {
- Character status;
- Boolean binary;
- String oldPath;
- Integer linesInserted;
- Integer linesDeleted;
- }
-
static class LabelInfo {
transient SubmitRecord.Label.Status _status;
@@ -954,6 +992,7 @@
static class ApprovalInfo extends AccountInfo {
Integer value;
+ Timestamp date;
ApprovalInfo(Account.Id id) {
super(id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
index be3081a..87910bf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -14,13 +14,22 @@
package com.google.gerrit.server.change;
+import com.google.common.base.Objects;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestResource.HasETag;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectState;
import com.google.inject.TypeLiteral;
-public class ChangeResource implements RestResource {
+import org.eclipse.jgit.lib.ObjectId;
+
+public class ChangeResource implements RestResource, HasETag {
public static final TypeLiteral<RestView<ChangeResource>> CHANGE_KIND =
new TypeLiteral<RestView<ChangeResource>>() {};
@@ -41,4 +50,23 @@
public Change getChange() {
return getControl().getChange();
}
+
+ @Override
+ public String getETag() {
+ CurrentUser user = control.getCurrentUser();
+ Hasher h = Hashing.md5().newHasher()
+ .putLong(getChange().getLastUpdatedOn().getTime())
+ .putInt(getChange().getRowVersion())
+ .putInt(user.isIdentifiedUser()
+ ? ((IdentifiedUser) user).getAccountId().get()
+ : 0);
+
+ byte[] buf = new byte[20];
+ for (ProjectState p : control.getProjectControl().getProjectState().tree()) {
+ ObjectId id = p.getConfig().getRevision();
+ Objects.firstNonNull(id, ObjectId.zeroId()).copyRawTo(buf, 0);
+ h.putBytes(buf);
+ }
+ return h.hash().toString();
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
new file mode 100644
index 0000000..18f45bb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.CherryPick.Input;
+import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.RefControl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+class CherryPick implements RestModifyView<RevisionResource, Input>,
+ UiAction<RevisionResource> {
+ private final Provider<ReviewDb> dbProvider;
+ private final Provider<CherryPickChange> cherryPickChange;
+ private final ChangeJson json;
+
+ static class Input {
+ String message;
+ String destination;
+ }
+
+ @Inject
+ CherryPick(Provider<ReviewDb> dbProvider,
+ Provider<CherryPickChange> cherryPickChange,
+ ChangeJson json) {
+ this.dbProvider = dbProvider;
+ this.cherryPickChange = cherryPickChange;
+ this.json = json;
+ }
+
+ @Override
+ public Object apply(RevisionResource revision, Input input)
+ throws AuthException, BadRequestException, ResourceConflictException,
+ Exception {
+ final ChangeControl control = revision.getControl();
+
+ if (input.message == null || input.message.trim().isEmpty()) {
+ throw new BadRequestException("message must be non-empty");
+ } else if (input.destination == null || input.destination.trim().isEmpty()) {
+ throw new BadRequestException("destination must be non-empty");
+ }
+
+ ReviewDb db = dbProvider.get();
+ if (!control.isVisible(db)) {
+ throw new AuthException("Cherry pick not permitted");
+ }
+
+ String refName = input.destination;
+ if (!refName.startsWith("refs/")) {
+ refName = "refs/heads/" + input.destination;
+ }
+
+ RefControl refControl = control.getProjectControl().controlForRef(refName);
+ if (!refControl.canUpload()) {
+ throw new AuthException("Not allowed to cherry pick "
+ + revision.getChange().getId().toString() + " to "
+ + input.destination);
+ }
+
+ final PatchSet.Id patchSetId = revision.getPatchSet().getId();
+ try {
+ Change.Id cherryPickedChangeId = cherryPickChange.get().cherryPick(
+ patchSetId, input.message,
+ input.destination, refControl);
+ return json.format(cherryPickedChangeId);
+ } catch (InvalidChangeOperationException e) {
+ throw new BadRequestException(e.getMessage());
+ } catch (MergeException e) {
+ throw new ResourceConflictException(e.getMessage());
+ }
+ }
+
+ @Override
+ public UiAction.Description getDescription(RevisionResource resource) {
+ return new UiAction.Description()
+ .setLabel("Cherry Pick")
+ .setTitle("Cherry pick change to a different branch")
+ .setVisible(resource.getControl().getProjectControl().canUpload());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
new file mode 100644
index 0000000..8b7b6ad
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -0,0 +1,258 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.ssh.NoSshInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterKey;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.ChangeIdUtil;
+
+import java.io.IOException;
+import java.util.List;
+
+public class CherryPickChange {
+
+ private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
+
+ private final ReviewDb db;
+ private final GitRepositoryManager gitManager;
+ private final PersonIdent myIdent;
+ private final IdentifiedUser currentUser;
+ private final CommitValidators.Factory commitValidatorsFactory;
+ private final ChangeInserter.Factory changeInserterFactory;
+ private final PatchSetInserter.Factory patchSetInserterFactory;
+ final MergeUtil.Factory mergeUtilFactory;
+
+ @Inject
+ CherryPickChange(final ReviewDb db, @GerritPersonIdent final PersonIdent myIdent,
+ final GitRepositoryManager gitManager, final IdentifiedUser currentUser,
+ final CommitValidators.Factory commitValidatorsFactory,
+ final ChangeInserter.Factory changeInserterFactory,
+ final PatchSetInserter.Factory patchSetInserterFactory,
+ final MergeUtil.Factory mergeUtilFactory) {
+ this.db = db;
+ this.gitManager = gitManager;
+ this.myIdent = myIdent;
+ this.currentUser = currentUser;
+ this.commitValidatorsFactory = commitValidatorsFactory;
+ this.changeInserterFactory = changeInserterFactory;
+ this.patchSetInserterFactory = patchSetInserterFactory;
+ this.mergeUtilFactory = mergeUtilFactory;
+ }
+
+ public Change.Id cherryPick(final PatchSet.Id patchSetId,
+ final String message, final String destinationBranch,
+ final RefControl refControl) throws NoSuchChangeException,
+ EmailException, OrmException, MissingObjectException,
+ IncorrectObjectTypeException, IOException,
+ InvalidChangeOperationException, MergeException {
+
+ final Change.Id changeId = patchSetId.getParentKey();
+ final PatchSet patch = db.patchSets().get(patchSetId);
+ if (patch == null) {
+ throw new NoSuchChangeException(changeId);
+ }
+ if (destinationBranch == null || destinationBranch.length() == 0) {
+ throw new InvalidChangeOperationException(
+ "Cherry Pick: Destination branch cannot be null or empty");
+ }
+
+ Project.NameKey project = db.changes().get(changeId).getProject();
+ final Repository git;
+ try {
+ git = gitManager.openRepository(project);
+ } catch (RepositoryNotFoundException e) {
+ throw new NoSuchChangeException(changeId, e);
+ }
+
+ try {
+ RevWalk revWalk = new RevWalk(git);
+ try {
+ Ref destRef = git.getRef(destinationBranch);
+ if (destRef == null) {
+ throw new InvalidChangeOperationException("Branch "
+ + destinationBranch + " does not exist.");
+ }
+
+ final RevCommit mergeTip = revWalk.parseCommit(destRef.getObjectId());
+
+ RevCommit commitToCherryPick =
+ revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
+
+ PersonIdent committerIdent =
+ currentUser.newCommitterIdent(myIdent.getWhen(),
+ myIdent.getTimeZone());
+
+ RevCommit cherryPickCommit;
+ ObjectInserter oi = git.newObjectInserter();
+ try {
+ ProjectState projectState = refControl.getProjectControl().getProjectState();
+ cherryPickCommit =
+ mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
+ commitToCherryPick, committerIdent, message, revWalk);
+ } finally {
+ oi.release();
+ }
+
+ if (cherryPickCommit == null) {
+ throw new MergeException(
+ "Could not create a merge commit during the cherry pick");
+ }
+
+ Change.Key changeKey;
+ final List<String> idList = cherryPickCommit.getFooterLines(CHANGE_ID);
+ if (!idList.isEmpty()) {
+ final String idStr = idList.get(idList.size() - 1).trim();
+ changeKey = new Change.Key(idStr);
+ } else {
+ final ObjectId computedChangeId =
+ ChangeIdUtil
+ .computeChangeId(cherryPickCommit.getTree(), mergeTip,
+ cherryPickCommit.getAuthorIdent(), myIdent, message);
+
+ changeKey = new Change.Key("I" + computedChangeId.name());
+ }
+
+ List<Change> destChanges =
+ db.changes()
+ .byBranchKey(
+ new Branch.NameKey(db.changes().get(changeId).getProject(),
+ destRef.getName()), changeKey).toList();
+
+ if (destChanges.size() > 1) {
+ throw new InvalidChangeOperationException("Several changes with key "
+ + changeKey + " resides on the same branch. "
+ + "Cannot create a new patch set.");
+ } else if (destChanges.size() == 1) {
+ // The change key exists on the destination branch. The cherry pick
+ // will be added as a new patch set.
+ return insertPatchSet(git, revWalk, destChanges.get(0), patchSetId,
+ cherryPickCommit, refControl);
+ } else {
+ // Change key not found on destination branch. We can create a new
+ // change.
+ return createNewChange(git, revWalk, changeKey, project, patchSetId, destRef,
+ cherryPickCommit, refControl);
+ }
+ } finally {
+ revWalk.release();
+ }
+ } finally {
+ git.close();
+ }
+ }
+
+ private Change.Id insertPatchSet(Repository git, RevWalk revWalk, Change change,
+ PatchSet.Id patchSetId, RevCommit cherryPickCommit,
+ RefControl refControl) throws InvalidChangeOperationException,
+ IOException, OrmException, NoSuchChangeException {
+ patchSetInserterFactory
+ .create(git, revWalk, refControl, currentUser, change, cherryPickCommit)
+ .setMessage(buildChangeMessage(patchSetId, change))
+ .insert();
+ return change.getId();
+ }
+
+ private Change.Id createNewChange(Repository git, RevWalk revWalk,
+ Change.Key changeKey, Project.NameKey project, PatchSet.Id patchSetId,
+ Ref destRef, RevCommit cherryPickCommit, RefControl refControl)
+ throws OrmException, InvalidChangeOperationException, IOException {
+ Change change =
+ new Change(changeKey, new Change.Id(db.nextChangeId()),
+ currentUser.getAccountId(), new Branch.NameKey(project,
+ destRef.getName()));
+ ChangeInserter ins =
+ changeInserterFactory.create(refControl, change, cherryPickCommit);
+ PatchSet newPatchSet = ins.getPatchSet();
+
+ CommitValidators commitValidators =
+ commitValidatorsFactory.create(refControl, new NoSshInfo(), git);
+ CommitReceivedEvent commitReceivedEvent =
+ new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
+ cherryPickCommit.getId(), newPatchSet.getRefName()), refControl
+ .getProjectControl().getProject(), refControl.getRefName(),
+ cherryPickCommit, currentUser);
+
+ try {
+ commitValidators.validateForGerritCommits(commitReceivedEvent);
+ } catch (CommitValidationException e) {
+ throw new InvalidChangeOperationException(e.getMessage());
+ }
+
+ final RefUpdate ru = git.updateRef(newPatchSet.getRefName());
+ ru.setExpectedOldObjectId(ObjectId.zeroId());
+ ru.setNewObjectId(cherryPickCommit);
+ ru.disableRefLog();
+ if (ru.update(revWalk) != RefUpdate.Result.NEW) {
+ throw new IOException(String.format(
+ "Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
+ change.getDest().getParentKey().get(), ru.getResult()));
+ }
+
+ ins.setMessage(buildChangeMessage(patchSetId, change)).insert();
+
+ return change.getId();
+ }
+
+ private ChangeMessage buildChangeMessage(PatchSet.Id patchSetId, Change dest)
+ throws OrmException {
+ ChangeMessage cmsg =
+ new ChangeMessage(new ChangeMessage.Key(patchSetId.getParentKey(),
+ ChangeUtil.messageUUID(db)), currentUser.getAccountId(),
+ patchSetId);
+ StringBuilder msgBuf =
+ new StringBuilder("Patch Set " + patchSetId.get()
+ + ": Cherry Picked");
+ msgBuf.append("\n\n");
+ msgBuf.append("This patchset was cherry picked to change: "
+ + dest.getKey().get());
+ cmsg.setMessage(msgBuf.toString());
+ return cmsg;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
index 798b9c6..fe372f3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentInfo.java
@@ -15,16 +15,15 @@
package com.google.gerrit.server.change;
import com.google.common.base.Strings;
+import com.google.gerrit.common.changes.Side;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.CommentRange;
import com.google.gerrit.server.account.AccountInfo;
import java.sql.Timestamp;
public class CommentInfo {
- static enum Side {
- PARENT, REVISION;
- }
final String kind = "gerritcodereview#comment";
String id;
@@ -35,6 +34,7 @@
String message;
Timestamp updated;
AccountInfo author;
+ CommentRange range;
CommentInfo(PatchLineComment c, AccountInfo.Loader accountLoader) {
id = Url.encode(c.getKey().get());
@@ -48,6 +48,7 @@
inReplyTo = Url.encode(c.getParentUuid());
message = Strings.emptyToNull(c.getMessage());
updated = c.getWrittenOn();
+ range = c.getRange();
if (accountLoader != null) {
author = accountLoader.get(c.getAuthor());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
index 5157af4..1b7bbe7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.change;
import com.google.common.base.Strings;
+import com.google.gerrit.common.changes.Side;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -23,7 +24,6 @@
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.change.PutDraft.Input;
@@ -50,18 +50,22 @@
throw new BadRequestException("message must be non-empty");
} else if (in.line != null && in.line <= 0) {
throw new BadRequestException("line must be > 0");
+ } else if (in.line != null && in.range != null && in.line != in.range.getEndLine()) {
+ throw new BadRequestException("range endLine must be on the same line as the comment");
}
+ int line = in.line != null
+ ? in.line
+ : in.range != null ? in.range.getEndLine() : 0;
+
PatchLineComment c = new PatchLineComment(
new PatchLineComment.Key(
new Patch.Key(rsrc.getPatchSet().getId(), in.path),
ChangeUtil.messageUUID(db.get())),
- in.line != null ? in.line : 0,
- rsrc.getAccountId(),
- Url.decode(in.inReplyTo));
- c.setStatus(Status.DRAFT);
- c.setSide(in.side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
+ line, rsrc.getAccountId(), Url.decode(in.inReplyTo));
+ c.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
c.setMessage(in.message.trim());
+ c.setRange(in.range);
db.get().patchComments().insert(Collections.singleton(c));
return Response.created(new CommentInfo(c, null));
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
new file mode 100644
index 0000000..f47c278
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.change.DeleteDraftChange.Input;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+
+public class DeleteDraftChange implements RestModifyView<ChangeResource, Input> {
+ public static class Input {
+ }
+
+ protected final Provider<ReviewDb> dbProvider;
+ private final GitRepositoryManager gitManager;
+ private final GitReferenceUpdated gitRefUpdated;
+
+ @Inject
+ public DeleteDraftChange(Provider<ReviewDb> dbProvider,
+ GitRepositoryManager gitManager,
+ GitReferenceUpdated gitRefUpdated,
+ PatchSetInfoFactory patchSetInfoFactory) {
+ this.dbProvider = dbProvider;
+ this.gitManager = gitManager;
+ this.gitRefUpdated = gitRefUpdated;
+ }
+
+ @Override
+ public Object apply(ChangeResource rsrc, Input input)
+ throws ResourceConflictException, AuthException,
+ ResourceNotFoundException, OrmException, IOException {
+ if (rsrc.getChange().getStatus() != Status.DRAFT) {
+ throw new ResourceConflictException("Change is not a draft");
+ }
+
+ if (!rsrc.getControl().canDeleteDraft(dbProvider.get())) {
+ throw new AuthException("Not permitted to delete this draft change");
+ }
+
+ try {
+ ChangeUtil.deleteDraftChange(rsrc.getChange().getId(),
+ gitManager, gitRefUpdated, dbProvider.get());
+ } catch (NoSuchChangeException e) {
+ throw new ResourceNotFoundException(e.getMessage());
+ }
+
+ return Response.none();
+ }
+
+ static class Action extends DeleteDraftChange implements UiAction<ChangeResource> {
+ @Inject
+ public Action(Provider<ReviewDb> dbProvider,
+ GitRepositoryManager gitManager,
+ GitReferenceUpdated gitRefUpdated,
+ PatchSetInfoFactory patchSetInfoFactory) {
+ super(dbProvider, gitManager, gitRefUpdated, patchSetInfoFactory);
+ }
+
+ @Override
+ public UiAction.Description getDescription(ChangeResource rsrc) {
+ try {
+ return new UiAction.Description()
+ .setTitle(String.format("Delete Draft Change %d",
+ rsrc.getChange().getChangeId()))
+ .setVisible(rsrc.getChange().getStatus() == Status.DRAFT
+ && rsrc.getControl().canDeleteDraft(dbProvider.get()));
+ } catch (OrmException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
new file mode 100644
index 0000000..dd20a38
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -0,0 +1,170 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.change.DeleteDraftPatchSet.Input;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+
+public class DeleteDraftPatchSet implements RestModifyView<RevisionResource, Input> {
+ public static class Input {
+ }
+
+ protected final Provider<ReviewDb> dbProvider;
+ private final GitRepositoryManager gitManager;
+ private final GitReferenceUpdated gitRefUpdated;
+ private final PatchSetInfoFactory patchSetInfoFactory;
+
+ @Inject
+ public DeleteDraftPatchSet(Provider<ReviewDb> dbProvider,
+ GitRepositoryManager gitManager,
+ GitReferenceUpdated gitRefUpdated,
+ PatchSetInfoFactory patchSetInfoFactory) {
+ this.dbProvider = dbProvider;
+ this.gitManager = gitManager;
+ this.gitRefUpdated = gitRefUpdated;
+ this.patchSetInfoFactory = patchSetInfoFactory;
+ }
+
+ @Override
+ public Object apply(RevisionResource rsrc, Input input)
+ throws ResourceNotFoundException, AuthException, OrmException,
+ IOException, ResourceConflictException {
+ PatchSet patchSet = rsrc.getPatchSet();
+ PatchSet.Id patchSetId = patchSet.getId();
+ Change change = rsrc.getChange();
+
+ if (!patchSet.isDraft()) {
+ throw new ResourceConflictException("Patch set is not a draft.");
+ }
+
+ if (!rsrc.getControl().canDeleteDraft(dbProvider.get())) {
+ throw new AuthException("Not permitted to delete this draft patch set");
+ }
+
+ deleteDraftPatchSet(patchSet, change);
+ deleteOrUpdateDraftChange(patchSetId, change);
+
+ return Response.none();
+ }
+
+ private void deleteDraftPatchSet(PatchSet patchSet, Change change)
+ throws ResourceNotFoundException, OrmException, IOException {
+ try {
+ ChangeUtil.deleteOnlyDraftPatchSet(patchSet,
+ change, gitManager, gitRefUpdated, dbProvider.get());
+ } catch (NoSuchChangeException e) {
+ throw new ResourceNotFoundException(e.getMessage());
+ }
+ }
+
+ private void deleteOrUpdateDraftChange(PatchSet.Id patchSetId,
+ Change change) throws OrmException, ResourceNotFoundException,
+ IOException {
+ if (dbProvider.get()
+ .patchSets()
+ .byChange(change.getId())
+ .toList().size() == 0) {
+ deleteDraftChange(patchSetId);
+ } else {
+ if (change.currentPatchSetId().equals(patchSetId)) {
+ updateCurrentPatchSet(dbProvider.get(), change,
+ previousPatchSetInfo(patchSetId));
+ }
+ }
+ }
+
+ private void deleteDraftChange(PatchSet.Id patchSetId)
+ throws OrmException, IOException, ResourceNotFoundException {
+ try {
+ ChangeUtil.deleteDraftChange(patchSetId,
+ gitManager, gitRefUpdated, dbProvider.get());
+ } catch (NoSuchChangeException e) {
+ throw new ResourceNotFoundException(e.getMessage());
+ }
+ }
+
+ private PatchSetInfo previousPatchSetInfo(PatchSet.Id patchSetId)
+ throws ResourceNotFoundException {
+ try {
+ return patchSetInfoFactory.get(dbProvider.get(),
+ new PatchSet.Id(patchSetId.getParentKey(),
+ patchSetId.get() - 1));
+ } catch (PatchSetInfoNotAvailableException e) {
+ throw new ResourceNotFoundException(e.getMessage());
+ }
+ }
+
+ private static void updateCurrentPatchSet(final ReviewDb db,
+ final Change change, final PatchSetInfo psInfo)
+ throws OrmException {
+ db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change c) {
+ c.setCurrentPatchSet(psInfo);
+ ChangeUtil.updated(c);
+ return c;
+ }
+ });
+ }
+
+ static class Action extends DeleteDraftPatchSet implements UiAction<RevisionResource> {
+ @Inject
+ public Action(Provider<ReviewDb> dbProvider,
+ GitRepositoryManager gitManager,
+ GitReferenceUpdated gitRefUpdated,
+ PatchSetInfoFactory patchSetInfoFactory) {
+ super(dbProvider, gitManager, gitRefUpdated, patchSetInfoFactory);
+ }
+
+ @Override
+ public UiAction.Description getDescription(RevisionResource rsrc) {
+ PatchSet.Id current = rsrc.getChange().currentPatchSetId();
+ try {
+ int psCount = dbProvider.get().patchSets()
+ .byChange(rsrc.getChange().getId()).toList().size();
+ return new UiAction.Description()
+ .setTitle(String.format("Delete Draft Revision %d",
+ rsrc.getPatchSet().getPatchSetId()))
+ .setVisible(rsrc.getPatchSet().isDraft()
+ && rsrc.getPatchSet().getId().equals(current)
+ && rsrc.getControl().canDeleteDraft(dbProvider.get())
+ && psCount > 1);
+ } catch (OrmException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
index 5bfffac..ac09374 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -25,6 +25,7 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.change.DeleteReviewer.Input;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gwtorm.server.OrmException;
@@ -63,6 +64,7 @@
if (del.isEmpty()) {
throw new ResourceNotFoundException();
}
+ ChangeUtil.bumpRowVersionNotLastUpdatedOn(rsrc.getChange().getId(), db);
db.patchSetApprovals().delete(del);
db.commit();
} finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
index 8282d7c..8fc05be 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
@@ -23,7 +23,6 @@
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -73,7 +72,7 @@
}
private void checkIdentifiedUser() throws AuthException {
- if (!(user.get() instanceof IdentifiedUser)) {
+ if (!(user.get().isIdentifiedUser())) {
throw new AuthException("drafts only available to authenticated users");
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EditMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EditMessage.java
new file mode 100644
index 0000000..a634b7c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/EditMessage.java
@@ -0,0 +1,126 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.change.EditMessage.Input;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mail.CommitMessageEditedSender;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+class EditMessage implements RestModifyView<RevisionResource, Input>,
+ UiAction<RevisionResource> {
+
+ private final Provider<ReviewDb> dbProvider;
+ private final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory;
+ private final GitRepositoryManager gitManager;
+ private final PersonIdent myIdent;
+ private final PatchSetInserter.Factory patchSetInserterFactory;
+ private final ChangeJson json;
+
+ static class Input {
+ @DefaultInput
+ String message;
+ }
+
+ @Inject
+ EditMessage(final Provider<ReviewDb> dbProvider,
+ final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory,
+ final GitRepositoryManager gitManager,
+ final PatchSetInserter.Factory patchSetInserterFactory,
+ @GerritPersonIdent final PersonIdent myIdent,
+ ChangeJson json) {
+ this.dbProvider = dbProvider;
+ this.commitMessageEditedSenderFactory = commitMessageEditedSenderFactory;
+ this.gitManager = gitManager;
+ this.myIdent = myIdent;
+ this.patchSetInserterFactory = patchSetInserterFactory;
+ this.json = json;
+ }
+
+ @Override
+ public ChangeInfo apply(RevisionResource rsrc, Input input)
+ throws BadRequestException, ResourceConflictException, EmailException,
+ OrmException, ResourceNotFoundException, IOException {
+ if (Strings.isNullOrEmpty(input.message)) {
+ throw new BadRequestException("message must be non-empty");
+ }
+
+ final Repository git;
+ try {
+ git = gitManager.openRepository(rsrc.getChange().getProject());
+ } catch (RepositoryNotFoundException e) {
+ throw new ResourceNotFoundException(e.getMessage());
+ }
+
+ try {
+ return json.format(ChangeUtil.editCommitMessage(
+ rsrc.getPatchSet().getId(),
+ rsrc.getControl().getRefControl(),
+ (IdentifiedUser) rsrc.getControl().getCurrentUser(),
+ input.message, dbProvider.get(),
+ commitMessageEditedSenderFactory, git, myIdent,
+ patchSetInserterFactory));
+ } catch (InvalidChangeOperationException e) {
+ throw new BadRequestException(e.getMessage());
+ } catch (MissingObjectException e) {
+ throw new ResourceConflictException(e.getMessage());
+ } catch (IncorrectObjectTypeException e) {
+ throw new ResourceConflictException(e.getMessage());
+ } catch (PatchSetInfoNotAvailableException e) {
+ throw new ResourceConflictException(e.getMessage());
+ } catch (NoSuchChangeException e) {
+ throw new ResourceNotFoundException();
+ } finally {
+ git.close();
+ }
+ }
+
+ @Override
+ public UiAction.Description getDescription(RevisionResource resource) {
+ PatchSet.Id current = resource.getChange().currentPatchSetId();
+ return new UiAction.Description()
+ .setLabel("Edit commit message")
+ .setVisible(resource.getChange().getStatus().isOpen()
+ && resource.getPatchSet().getId().equals(current)
+ && resource.getControl().canAddPatchSet());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
new file mode 100644
index 0000000..fec72d6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+public class FileInfoJson {
+ private final PatchListCache patchListCache;
+
+ @Inject
+ FileInfoJson(PatchListCache patchListCache) {
+ this.patchListCache = patchListCache;
+ }
+
+ Map<String, FileInfo> toFileInfoMap(Change change, PatchSet patchSet)
+ throws PatchListNotAvailableException {
+ return toFileInfoMap(change, patchSet, null);
+ }
+
+ Map<String, FileInfo> toFileInfoMap(Change change, PatchSet patchSet, @Nullable PatchSet base)
+ throws PatchListNotAvailableException {
+ ObjectId a = (base == null)
+ ? null
+ : ObjectId.fromString(base.getRevision().get());
+ ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
+ PatchList list = patchListCache.get(
+ new PatchListKey(change.getProject(), a, b, Whitespace.IGNORE_NONE));
+
+ Map<String, FileInfo> files = Maps.newTreeMap();
+ for (PatchListEntry e : list.getPatches()) {
+ FileInfoJson.FileInfo d = new FileInfoJson.FileInfo();
+ d.status = e.getChangeType() != Patch.ChangeType.MODIFIED
+ ? e.getChangeType().getCode() : null;
+ d.oldPath = e.getOldName();
+ if (e.getPatchType() == Patch.PatchType.BINARY) {
+ d.binary = true;
+ } else {
+ d.linesInserted = e.getInsertions() > 0 ? e.getInsertions() : null;
+ d.linesDeleted = e.getDeletions() > 0 ? e.getDeletions() : null;
+ }
+
+ FileInfoJson.FileInfo o = files.put(e.getNewName(), d);
+ if (o != null) {
+ // This should only happen on a delete-add break created by JGit
+ // when the file was rewritten and too little content survived. Write
+ // a single record with data from both sides.
+ d.status = Patch.ChangeType.REWRITE.getCode();
+ if (o.binary != null && o.binary) {
+ d.binary = true;
+ }
+ if (o.linesInserted != null) {
+ d.linesInserted = o.linesInserted;
+ }
+ if (o.linesDeleted != null) {
+ d.linesDeleted = o.linesDeleted;
+ }
+ }
+ }
+ return files;
+ }
+
+ static class FileInfo {
+ Character status;
+ Boolean binary;
+ String oldPath;
+ Integer linesInserted;
+ Integer linesDeleted;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
new file mode 100644
index 0000000..521e8c8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.inject.TypeLiteral;
+
+public class FileResource implements RestResource {
+ public static final TypeLiteral<RestView<FileResource>> FILE_KIND =
+ new TypeLiteral<RestView<FileResource>>() {};
+
+ private final RevisionResource rev;
+ private final Patch.Key key;
+
+ FileResource(RevisionResource rev, String name) {
+ this.rev = rev;
+ this.key = new Patch.Key(rev.getPatchSet().getId(), name);
+ }
+
+ public Patch.Key getPatchKey() {
+ return key;
+ }
+
+ public boolean isCacheable() {
+ return rev.isCacheable();
+ }
+
+ Account.Id getAccountId() {
+ return rev.getAccountId();
+ }
+
+ RevisionResource getRevision() {
+ return rev;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
new file mode 100644
index 0000000..ec8234f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -0,0 +1,251 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.CacheControl;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountPatchReview;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.FileInfoJson.FileInfo;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.concurrent.TimeUnit;
+
+class Files implements ChildCollection<RevisionResource, FileResource> {
+ private final DynamicMap<RestView<FileResource>> views;
+ private final Provider<ListFiles> list;
+
+ @Inject
+ Files(DynamicMap<RestView<FileResource>> views, Provider<ListFiles> list) {
+ this.views = views;
+ this.list = list;
+ }
+
+ @Override
+ public DynamicMap<RestView<FileResource>> views() {
+ return views;
+ }
+
+ @Override
+ public RestView<RevisionResource> list() throws AuthException {
+ return list.get();
+ }
+
+ @Override
+ public FileResource parse(RevisionResource rev, IdString id)
+ throws ResourceNotFoundException, OrmException, AuthException {
+ return new FileResource(rev, id.get());
+ }
+
+ private static final class ListFiles implements RestReadView<RevisionResource> {
+ private static final Logger log = LoggerFactory.getLogger(ListFiles.class);
+
+ @Option(name = "--base", metaVar = "revision-id")
+ String base;
+
+ @Option(name = "--reviewed")
+ boolean reviewed;
+
+ private final Provider<ReviewDb> db;
+ private final Provider<CurrentUser> self;
+ private final FileInfoJson fileInfoJson;
+ private final Provider<Revisions> revisions;
+ private final GitRepositoryManager gitManager;
+ private final PatchListCache patchListCache;
+
+ @Inject
+ ListFiles(Provider<ReviewDb> db,
+ Provider<CurrentUser> self,
+ FileInfoJson fileInfoJson,
+ Provider<Revisions> revisions,
+ GitRepositoryManager gitManager,
+ PatchListCache patchListCache) {
+ this.db = db;
+ this.self = self;
+ this.fileInfoJson = fileInfoJson;
+ this.revisions = revisions;
+ this.gitManager = gitManager;
+ this.patchListCache = patchListCache;
+ }
+
+ @Override
+ public Object apply(RevisionResource resource)
+ throws ResourceNotFoundException, OrmException,
+ PatchListNotAvailableException, BadRequestException, AuthException {
+ if (base != null && reviewed) {
+ throw new BadRequestException("cannot combine base and reviewed");
+ } else if (reviewed) {
+ return reviewed(resource);
+ }
+
+ PatchSet basePatchSet = null;
+ if (base != null) {
+ RevisionResource baseResource = revisions.get().parse(
+ resource.getChangeResource(), IdString.fromDecoded(base));
+ basePatchSet = baseResource.getPatchSet();
+ }
+ Response<Map<String, FileInfo>> r = Response.ok(fileInfoJson.toFileInfoMap(
+ resource.getChange(),
+ resource.getPatchSet(),
+ basePatchSet));
+ if (resource.isCacheable()) {
+ r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+ }
+ return r;
+ }
+
+ private Object reviewed(RevisionResource resource)
+ throws AuthException, OrmException {
+ CurrentUser user = self.get();
+ if (!(user.isIdentifiedUser())) {
+ throw new AuthException("Authentication required");
+ }
+
+ Account.Id userId = ((IdentifiedUser) user).getAccountId();
+ List<String> r = scan(userId, resource.getPatchSet().getId());
+
+ if (r.isEmpty() && 1 < resource.getPatchSet().getPatchSetId()) {
+ for (Integer id : reverseSortPatchSets(resource)) {
+ PatchSet.Id old = new PatchSet.Id(resource.getChange().getId(), id);
+ List<String> o = scan(userId, old);
+ if (!o.isEmpty()) {
+ try {
+ r = copy(Sets.newHashSet(o), old, resource, userId);
+ } catch (IOException e) {
+ log.warn("Cannot copy patch review flags", e);
+ } catch (PatchListNotAvailableException e) {
+ log.warn("Cannot copy patch review flags", e);
+ }
+ break;
+ }
+ }
+ }
+
+ return r;
+ }
+
+ private List<String> scan(Account.Id userId, PatchSet.Id psId)
+ throws OrmException {
+ List<String> r = Lists.newArrayList();
+ for (AccountPatchReview w : db.get().accountPatchReviews()
+ .byReviewer(userId, psId)) {
+ r.add(w.getKey().getPatchKey().getFileName());
+ }
+ return r;
+ }
+
+ private List<Integer> reverseSortPatchSets(
+ RevisionResource resource) throws OrmException {
+ SortedSet<Integer> ids = Sets.newTreeSet();
+ for (PatchSet p : db.get().patchSets()
+ .byChange(resource.getChange().getId())) {
+ if (p.getPatchSetId() < resource.getPatchSet().getPatchSetId()) {
+ ids.add(p.getPatchSetId());
+ }
+ }
+
+ List<Integer> r = Lists.newArrayList(ids);
+ Collections.reverse(r);
+ return r;
+ }
+
+ private List<String> copy(Set<String> paths, PatchSet.Id old,
+ RevisionResource resource, Account.Id userId) throws IOException,
+ PatchListNotAvailableException, OrmException {
+ Repository git =
+ gitManager.openRepository(resource.getChange().getProject());
+ try {
+ ObjectReader reader = git.newObjectReader();
+ try {
+ PatchList oldList = patchListCache.get(
+ resource.getChange(),
+ db.get().patchSets().get(old));
+
+ PatchList curList = patchListCache.get(
+ resource.getChange(),
+ resource.getPatchSet());
+
+ int sz = paths.size();
+ List<AccountPatchReview> inserts = Lists.newArrayListWithCapacity(sz);
+ List<String> pathList = Lists.newArrayListWithCapacity(sz);
+
+ RevWalk rw = new RevWalk(reader);
+ RevTree o = rw.parseCommit(oldList.getNewId()).getTree();
+ RevTree c = rw.parseCommit(curList.getNewId()).getTree();
+ for (PatchListEntry p : curList.getPatches()) {
+ String path = p.getNewName();
+ if (!Patch.COMMIT_MSG.equals(path) && paths.contains(path)) {
+ TreeWalk tw = TreeWalk.forPath(reader, path, o, c);
+ if (tw != null
+ && tw.getRawMode(0) != 0
+ && tw.getRawMode(1) != 0
+ && tw.idEqual(0, 1)) {
+ inserts.add(new AccountPatchReview(
+ new Patch.Key(
+ resource.getPatchSet().getId(),
+ path),
+ userId));
+ pathList.add(path);
+ }
+ }
+ }
+ db.get().accountPatchReviews().insert(inserts);
+ return pathList;
+ } finally {
+ reader.release();
+ }
+ } finally {
+ git.close();
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
new file mode 100644
index 0000000..7d29449
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.CacheControl;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.change.ChangeJson.CommitInfo;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import java.util.concurrent.TimeUnit;
+
+public class GetCommit implements RestReadView<RevisionResource> {
+ private final ChangeJson json;
+
+ @Inject
+ GetCommit(ChangeJson json) {
+ this.json = json;
+ }
+
+ @Override
+ public Response<CommitInfo> apply(RevisionResource resource)
+ throws OrmException, PatchSetInfoNotAvailableException {
+ Response<CommitInfo> r = Response.ok(json.toCommit(resource.getPatchSet()));
+ if (resource.isCacheable()) {
+ r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+ }
+ return r;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
new file mode 100644
index 0000000..f0d2297
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class GetContent implements RestReadView<FileResource> {
+ private final GitRepositoryManager repoManager;
+
+ @Inject
+ GetContent(GitRepositoryManager repoManager) {
+ this.repoManager = repoManager;
+ }
+
+ @Override
+ public BinaryResult apply(FileResource rsrc)
+ throws ResourceNotFoundException, IOException {
+ Project.NameKey project =
+ rsrc.getRevision().getControl().getProject().getNameKey();
+ Repository repo = repoManager.openRepository(project);
+ try {
+ RevWalk rw = new RevWalk(repo);
+ try {
+ RevCommit commit =
+ rw.parseCommit(ObjectId.fromString(rsrc.getRevision().getPatchSet()
+ .getRevision().get()));
+ TreeWalk tw =
+ TreeWalk.forPath(rw.getObjectReader(), rsrc.getPatchKey().get(),
+ commit.getTree().getId());
+ if (tw == null) {
+ throw new ResourceNotFoundException();
+ }
+ try {
+ final ObjectLoader object = repo.open(tw.getObjectId(0));
+ return new BinaryResult() {
+ @Override
+ public void writeTo(OutputStream os) throws IOException {
+ object.copyTo(os);
+ }
+ }.setContentLength(object.getSize())
+ .base64();
+ } finally {
+ tw.release();
+ }
+ } finally {
+ rw.release();
+ }
+ } finally {
+ repo.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
index b3cd813..2375fb0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
@@ -15,13 +15,29 @@
package com.google.gerrit.server.change;
import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.extensions.restapi.CacheControl;
+import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import org.kohsuke.args4j.Option;
+
+import java.util.concurrent.TimeUnit;
+
public class GetDetail implements RestReadView<ChangeResource> {
private final ChangeJson json;
+ @Option(name = "-o", multiValued = true, usage = "Output options")
+ void addOption(ListChangesOption o) {
+ json.addOption(o);
+ }
+
+ @Option(name = "-O", usage = "Output option flags, in hex")
+ void setOptionFlagsHex(String hex) {
+ json.addOptions(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
+ }
+
@Inject
GetDetail(ChangeJson json) {
this.json = json
@@ -33,6 +49,7 @@
@Override
public Object apply(ChangeResource rsrc) throws OrmException {
- return json.format(rsrc);
+ return Response.ok(json.format(rsrc))
+ .caching(CacheControl.PRIVATE(0, TimeUnit.SECONDS).setMustRevalidate());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
new file mode 100644
index 0000000..753a70b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -0,0 +1,346 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.common.data.PatchScript.DisplayMethod;
+import com.google.gerrit.common.data.PatchScript.FileMode;
+import com.google.gerrit.extensions.restapi.CacheControl;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.prettify.common.SparseFileContent;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.patch.PatchScriptFactory;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.git.LargeObjectException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import org.eclipse.jgit.diff.Edit;
+import org.eclipse.jgit.diff.ReplaceEdit;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.NamedOptionDef;
+import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class GetDiff implements RestReadView<FileResource> {
+ private final PatchScriptFactory.Factory patchScriptFactoryFactory;
+ private final Provider<Revisions> revisions;
+
+ @Option(name = "--base", metaVar = "REVISION")
+ String base;
+
+ @Option(name = "--ignore-whitespace")
+ IgnoreWhitespace ignoreWhitespace = IgnoreWhitespace.NONE;
+
+ @Option(name = "--context", handler = ContextOptionHandler.class)
+ short context = AccountDiffPreference.DEFAULT_CONTEXT;
+
+ @Option(name = "--intraline")
+ boolean intraline;
+
+ @Inject
+ GetDiff(PatchScriptFactory.Factory patchScriptFactoryFactory,
+ Provider<Revisions> revisions) {
+ this.patchScriptFactoryFactory = patchScriptFactoryFactory;
+ this.revisions = revisions;
+ }
+
+ @Override
+ public Object apply(FileResource resource)
+ throws OrmException, NoSuchChangeException, LargeObjectException, ResourceNotFoundException {
+ PatchSet.Id basePatchSet = null;
+ if (base != null) {
+ RevisionResource baseResource = revisions.get().parse(
+ resource.getRevision().getChangeResource(), IdString.fromDecoded(base));
+ basePatchSet = baseResource.getPatchSet().getId();
+ }
+ AccountDiffPreference prefs = new AccountDiffPreference(new Account.Id(0));
+ prefs.setIgnoreWhitespace(ignoreWhitespace.whitespace);
+ prefs.setContext(context);
+ prefs.setIntralineDifference(intraline);
+
+ PatchScript ps = patchScriptFactoryFactory.create(
+ resource.getRevision().getControl(),
+ resource.getPatchKey().getFileName(),
+ basePatchSet,
+ resource.getPatchKey().getParentKey(),
+ prefs)
+ .call();
+
+ Content content = new Content(ps);
+ for (Edit edit : ps.getEdits()) {
+ if (edit.getType() == Edit.Type.EMPTY) {
+ continue;
+ }
+ content.addCommon(edit.getBeginA());
+
+ checkState(content.nextA == edit.getBeginA(),
+ "nextA = %d; want %d", content.nextA, edit.getBeginA());
+ checkState(content.nextB == edit.getBeginB(),
+ "nextB = %d; want %d", content.nextB, edit.getBeginB());
+ switch (edit.getType()) {
+ case DELETE:
+ case INSERT:
+ case REPLACE:
+ List<Edit> internalEdit = edit instanceof ReplaceEdit
+ ? ((ReplaceEdit) edit).getInternalEdits()
+ : null;
+ content.addDiff(edit.getEndA(), edit.getEndB(), internalEdit);
+ break;
+ case EMPTY:
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ content.addCommon(ps.getA().size());
+
+ Result result = new Result();
+ if (ps.getDisplayMethodA() != DisplayMethod.NONE) {
+ result.metaA = new FileMeta();
+ result.metaA.name = Objects.firstNonNull(ps.getOldName(), ps.getNewName());
+ result.metaA.setContentType(ps.getFileModeA(), ps.getMimeTypeA());
+ }
+
+ if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
+ result.metaB = new FileMeta();
+ result.metaB.name = ps.getNewName();
+ result.metaB.setContentType(ps.getFileModeB(), ps.getMimeTypeB());
+ }
+
+ if (intraline) {
+ if (ps.hasIntralineTimeout()) {
+ result.intralineStatus = IntraLineStatus.TIMEOUT;
+ } else if (ps.hasIntralineFailure()) {
+ result.intralineStatus = IntraLineStatus.FAILURE;
+ } else {
+ result.intralineStatus = IntraLineStatus.OK;
+ }
+ }
+
+ result.changeType = ps.getChangeType();
+ if (ps.getPatchHeader().size() > 0) {
+ result.diffHeader = ps.getPatchHeader();
+ }
+ result.content = content.lines;
+ Response<Result> r = Response.ok(result);
+ if (resource.isCacheable()) {
+ r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+ }
+ return r;
+ }
+
+ static class Result {
+ FileMeta metaA;
+ FileMeta metaB;
+ IntraLineStatus intralineStatus;
+ ChangeType changeType;
+ List<String> diffHeader;
+ List<ContentEntry> content;
+ }
+
+ static class FileMeta {
+ String name;
+ String contentType;
+ String url;
+
+ void setContentType(FileMode fileMode, String mimeType) {
+ switch (fileMode) {
+ case FILE:
+ contentType = mimeType;
+ break;
+ case GITLINK:
+ contentType = "x-git/gitlink";
+ break;
+ case SYMLINK:
+ contentType = "x-git/symlink";
+ break;
+ default:
+ throw new IllegalStateException("file mode: " + fileMode);
+ }
+ }
+ }
+
+ enum IntraLineStatus {
+ OK,
+ TIMEOUT,
+ FAILURE;
+ }
+
+ private static class Content {
+ final List<ContentEntry> lines;
+ final SparseFileContent fileA;
+ final SparseFileContent fileB;
+
+ int nextA;
+ int nextB;
+
+ Content(PatchScript ps) {
+ lines = Lists.newArrayListWithExpectedSize(ps.getEdits().size() + 2);
+ fileA = ps.getA();
+ fileB = ps.getB();
+ }
+
+ void addCommon(int end) {
+ end = Math.min(end, fileA.size());
+ if (nextA >= end) {
+ return;
+ }
+ nextB += end - nextA;
+
+ while (nextA < end) {
+ if (fileA.contains(nextA)) {
+ ContentEntry e = entry();
+ e.ab = Lists.newArrayListWithCapacity(end - nextA);
+ for (int i = nextA; i == nextA && i < end; i = fileA.next(i), nextA++) {
+ e.ab.add(fileA.get(i));
+ }
+ } else {
+ int endRegion = Math.min(end,
+ (nextA == 0) ? fileA.first() : fileA.next(nextA - 1));
+ ContentEntry e = entry();
+ e.skip = endRegion - nextA;
+ nextA = endRegion;
+ }
+ }
+ }
+
+ void addDiff(int endA, int endB, List<Edit> internalEdit) {
+ int lenA = endA - nextA;
+ int lenB = endB - nextB;
+ checkState(lenA > 0 || lenB > 0);
+
+ ContentEntry e = entry();
+ if (lenA > 0) {
+ e.a = Lists.newArrayListWithCapacity(lenA);
+ for (; nextA < endA; nextA++) {
+ e.a.add(fileA.get(nextA));
+ }
+ }
+ if (lenB > 0) {
+ e.b = Lists.newArrayListWithCapacity(lenB);
+ for (; nextB < endB; nextB++) {
+ e.b.add(fileB.get(nextB));
+ }
+ }
+ if (internalEdit != null && !internalEdit.isEmpty()) {
+ e.editA = Lists.newArrayListWithCapacity(internalEdit.size() * 2);
+ e.editB = Lists.newArrayListWithCapacity(internalEdit.size() * 2);
+ int lastA = 0;
+ int lastB = 0;
+ for (Edit edit : internalEdit) {
+ if (edit.getBeginA() != edit.getEndA()) {
+ e.editA.add(ImmutableList.of(edit.getBeginA() - lastA, edit.getEndA() - edit.getBeginA()));
+ lastA = edit.getEndA();
+ }
+ if (edit.getBeginB() != edit.getEndB()) {
+ e.editB.add(ImmutableList.of(edit.getBeginB() - lastB, edit.getEndB() - edit.getBeginB()));
+ lastB = edit.getEndB();
+ }
+ }
+ }
+ }
+
+ private ContentEntry entry() {
+ ContentEntry e = new ContentEntry();
+ lines.add(e);
+ return e;
+ }
+ }
+
+ enum IgnoreWhitespace {
+ NONE(AccountDiffPreference.Whitespace.IGNORE_NONE),
+ TRAILING(AccountDiffPreference.Whitespace.IGNORE_SPACE_AT_EOL),
+ CHANGED(AccountDiffPreference.Whitespace.IGNORE_SPACE_CHANGE),
+ ALL(AccountDiffPreference.Whitespace.IGNORE_ALL_SPACE);
+
+ private final AccountDiffPreference.Whitespace whitespace;
+
+ private IgnoreWhitespace(AccountDiffPreference.Whitespace whitespace) {
+ this.whitespace = whitespace;
+ }
+ }
+
+ static final class ContentEntry {
+ // Common lines to both sides.
+ List<String> ab;
+ // Lines of a.
+ List<String> a;
+ // Lines of b.
+ List<String> b;
+
+ // A list of changed sections of the of the corresponding line list.
+ // Each entry is a character <offset, length> pair. The offset is from the
+ // beginning of the first line in the list. Also, the offset includes an
+ // implied trailing newline character for each line.
+ List<List<Integer>> editA;
+ List<List<Integer>> editB;
+
+ // Number of lines to skip on both sides.
+ Integer skip;
+ }
+
+ public static class ContextOptionHandler extends OptionHandler<Short> {
+ public ContextOptionHandler(
+ CmdLineParser parser, OptionDef option, Setter<Short> setter) {
+ super(parser, option, setter);
+ }
+
+ @Override
+ public final int parseArguments(final Parameters params)
+ throws CmdLineException {
+ final String value = params.getParameter(0);
+ short context;
+ if ("all".equalsIgnoreCase(value)) {
+ context = AccountDiffPreference.WHOLE_FILE_CONTEXT;
+ } else {
+ try {
+ context = Short.parseShort(value, 10);
+ if (context < 0) {
+ throw new NumberFormatException();
+ }
+ } catch (NumberFormatException e) {
+ throw new CmdLineException(owner,
+ String.format("\"%s\" is not a valid value for \"%s\"",
+ value, ((NamedOptionDef) option).name()));
+ }
+ }
+ setter.addValue(context);
+ return 1;
+ }
+
+ @Override
+ public final String getDefaultMetaVariable() {
+ return "ALL|# LINES";
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
new file mode 100644
index 0000000..4c1994b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetPatch.java
@@ -0,0 +1,177 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Charsets.UTF_8;
+
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+public class GetPatch implements RestReadView<RevisionResource> {
+ private final GitRepositoryManager repoManager;
+
+ @Option(name = "--zip")
+ private boolean zip;
+
+ @Option(name = "--download")
+ private boolean download;
+
+ @Inject
+ GetPatch(GitRepositoryManager repoManager) {
+ this.repoManager = repoManager;
+ }
+
+ @Override
+ public BinaryResult apply(RevisionResource rsrc)
+ throws ResourceNotFoundException, ResourceConflictException {
+ Project.NameKey project = rsrc.getControl().getProject().getNameKey();
+ boolean close = true;
+ try {
+ final Repository repo = repoManager.openRepository(project);
+ try {
+ final RevWalk rw = new RevWalk(repo);
+ try {
+ final RevCommit commit =
+ rw.parseCommit(ObjectId.fromString(rsrc.getPatchSet()
+ .getRevision().get()));
+ RevCommit[] parents = commit.getParents();
+ if (parents.length > 1) {
+ throw new ResourceConflictException(
+ "Revision has more than 1 parent.");
+ } else if (parents.length == 0) {
+ throw new ResourceConflictException("Revision has no parent.");
+ }
+ final RevCommit base = parents[0];
+ rw.parseBody(base);
+
+ BinaryResult bin = new BinaryResult() {
+ @Override
+ public void writeTo(OutputStream out) throws IOException {
+ if (zip) {
+ ZipOutputStream zos = new ZipOutputStream(out);
+ ZipEntry e = new ZipEntry(fileName(rw, commit));
+ e.setTime(commit.getCommitTime() * 1000L);
+ zos.putNextEntry(e);
+ format(zos);
+ zos.closeEntry();
+ zos.finish();
+ } else {
+ format(out);
+ }
+ }
+
+ private void format(OutputStream out) throws IOException {
+ out.write(formatEmailHeader(commit).getBytes(UTF_8));
+ DiffFormatter fmt = new DiffFormatter(out);
+ fmt.setRepository(repo);
+ fmt.format(base.getTree(), commit.getTree());
+ fmt.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ rw.release();
+ repo.close();
+ }
+ };
+
+ if (zip) {
+ bin.disableGzip()
+ .setContentType("application/zip")
+ .setAttachmentName(fileName(rw, commit) + ".zip");
+ } else {
+ bin.base64()
+ .setContentType("application/mbox")
+ .setAttachmentName(download
+ ? fileName(rw, commit) + ".base64"
+ : null);
+ }
+
+ close = false;
+ return bin;
+ } finally {
+ if (close) {
+ rw.release();
+ }
+ }
+ } finally {
+ if (close) {
+ repo.close();
+ }
+ }
+ } catch (IOException e) {
+ throw new ResourceNotFoundException();
+ }
+ }
+
+ private static String formatEmailHeader(RevCommit commit) {
+ StringBuilder b = new StringBuilder();
+ PersonIdent author = commit.getAuthorIdent();
+ String subject = commit.getShortMessage();
+ String msg = commit.getFullMessage().substring(subject.length());
+ if (msg.startsWith("\n\n")) {
+ msg = msg.substring(2);
+ }
+ b.append("From ").append(commit.getName())
+ .append(' ')
+ .append("Mon Sep 17 00:00:00 2001\n")
+ .append("From: ").append(author.getName())
+ .append(" <").append(author.getEmailAddress()).append(">\n")
+ .append("Date: ").append(formatDate(author)).append('\n')
+ .append("Subject: [PATCH] ").append(subject).append('\n')
+ .append('\n')
+ .append(msg);
+ if (!msg.endsWith("\n")) {
+ b.append('\n');
+ }
+ return b.append("---\n\n").toString();
+ }
+
+ private static String formatDate(PersonIdent author) {
+ SimpleDateFormat df = new SimpleDateFormat(
+ "EEE, dd MMM yyyy HH:mm:ss Z",
+ Locale.US);
+ df.setCalendar(Calendar.getInstance(author.getTimeZone(), Locale.US));
+ return df.format(author.getWhen());
+ }
+
+ private static String fileName(RevWalk rw, RevCommit commit)
+ throws IOException {
+ AbbreviatedObjectId id = rw.getObjectReader().abbreviate(commit, 8);
+ return id.name() + ".diff";
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
new file mode 100644
index 0000000..9c69259
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
@@ -0,0 +1,301 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ChangeJson.CommitInfo;
+import com.google.gerrit.server.change.ChangeJson.GitPerson;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+public class GetRelated implements RestReadView<RevisionResource> {
+ private static final Logger log = LoggerFactory.getLogger(GetRelated.class);
+
+ private final GitRepositoryManager gitMgr;
+ private final Provider<ReviewDb> dbProvider;
+
+ @Inject
+ GetRelated(GitRepositoryManager gitMgr, Provider<ReviewDb> db) {
+ this.gitMgr = gitMgr;
+ this.dbProvider = db;
+ }
+
+ @Override
+ public Object apply(RevisionResource rsrc)
+ throws RepositoryNotFoundException, IOException, OrmException {
+ Repository git = gitMgr.openRepository(rsrc.getChange().getProject());
+ try {
+ Ref ref = git.getRef(rsrc.getChange().getDest().get());
+ RevWalk rw = new RevWalk(git);
+ try {
+ RelatedInfo info = new RelatedInfo();
+ info.changes = walk(rsrc, rw, ref);
+ return info;
+ } finally {
+ rw.release();
+ }
+ } finally {
+ git.close();
+ }
+ }
+
+ private List<ChangeAndCommit> walk(RevisionResource rsrc, RevWalk rw, Ref ref)
+ throws OrmException, IOException {
+ Map<Change.Id, Change> changes = allOpenChanges(rsrc);
+ Map<PatchSet.Id, PatchSet> patchSets = allPatchSets(changes.keySet());
+ List<ChangeAndCommit> list = children(rsrc, rw, changes, patchSets);
+
+ Map<String, PatchSet> commits = Maps.newHashMap();
+ for (PatchSet p : patchSets.values()) {
+ commits.put(p.getRevision().get(), p);
+ }
+
+ RevCommit rev = rw.parseCommit(ObjectId.fromString(
+ rsrc.getPatchSet().getRevision().get()));
+ rw.sort(RevSort.TOPO);
+ rw.markStart(rev);
+
+ if (ref != null && ref.getObjectId() != null) {
+ try {
+ rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
+ } catch (IncorrectObjectTypeException notCommit) {
+ // Ignore and treat as new branch.
+ }
+ }
+
+ for (RevCommit c; (c = rw.next()) != null;) {
+ PatchSet p = commits.get(c.name());
+ Change g = p != null ? changes.get(p.getId().getParentKey()) : null;
+ list.add(new ChangeAndCommit(g, p, c));
+ }
+
+ if (list.size() == 1) {
+ ChangeAndCommit r = list.get(0);
+ if (r._changeNumber != null && r._revisionNumber != null
+ && r._changeNumber == rsrc.getChange().getChangeId()
+ && r._revisionNumber == rsrc.getPatchSet().getPatchSetId()) {
+ return Collections.emptyList();
+ }
+ }
+ return list;
+ }
+
+ private Map<Change.Id, Change> allOpenChanges(RevisionResource rsrc)
+ throws OrmException {
+ ReviewDb db = dbProvider.get();
+ return db.changes().toMap(
+ db.changes().byBranchOpenAll(rsrc.getChange().getDest()));
+ }
+
+ private Map<PatchSet.Id, PatchSet> allPatchSets(Collection<Change.Id> ids)
+ throws OrmException {
+ int n = ids.size();
+ ReviewDb db = dbProvider.get();
+ List<ResultSet<PatchSet>> t = Lists.newArrayListWithCapacity(n);
+ for (Change.Id id : ids) {
+ t.add(db.patchSets().byChange(id));
+ }
+
+ Map<PatchSet.Id, PatchSet> r = Maps.newHashMapWithExpectedSize(n * 2);
+ for (ResultSet<PatchSet> rs : t) {
+ for (PatchSet p : rs) {
+ r.put(p.getId(), p);
+ }
+ }
+ return r;
+ }
+
+ private List<ChangeAndCommit> children(RevisionResource rsrc, RevWalk rw,
+ Map<Change.Id, Change> changes, Map<PatchSet.Id, PatchSet> patchSets)
+ throws OrmException, IOException {
+ // children is a map of parent commit name to PatchSet built on it.
+ Multimap<String, PatchSet.Id> children = allChildren(changes.keySet());
+
+ RevFlag seenCommit = rw.newFlag("seenCommit");
+ LinkedList<String> q = Lists.newLinkedList();
+ seedQueue(rsrc, rw, seenCommit, patchSets, q);
+
+ ProjectControl projectCtl = rsrc.getControl().getProjectControl();
+ Set<Change.Id> seenChange = Sets.newHashSet();
+ List<ChangeAndCommit> graph = Lists.newArrayList();
+ while (!q.isEmpty()) {
+ String id = q.remove();
+
+ // For every matching change find the most recent patch set.
+ Map<Change.Id, PatchSet.Id> matches = Maps.newHashMap();
+ for (PatchSet.Id psId : children.get(id)) {
+ PatchSet.Id e = matches.get(psId.getParentKey());
+ if ((e == null || e.get() < psId.get())
+ && isVisible(projectCtl, changes, patchSets, psId)) {
+ matches.put(psId.getParentKey(), psId);
+ }
+ }
+
+ for (Map.Entry<Change.Id, PatchSet.Id> e : matches.entrySet()) {
+ Change change = changes.get(e.getKey());
+ PatchSet ps = patchSets.get(e.getValue());
+ if (change == null || ps == null || !seenChange.add(e.getKey())) {
+ continue;
+ }
+
+ RevCommit c = rw.parseCommit(ObjectId.fromString(
+ ps.getRevision().get()));
+ if (!c.has(seenCommit)) {
+ c.add(seenCommit);
+ q.addFirst(ps.getRevision().get());
+ graph.add(new ChangeAndCommit(change, ps, c));
+ }
+ }
+ }
+ Collections.reverse(graph);
+ return graph;
+ }
+
+ private boolean isVisible(ProjectControl projectCtl,
+ Map<Change.Id, Change> changes,
+ Map<PatchSet.Id, PatchSet> patchSets,
+ PatchSet.Id psId) throws OrmException {
+ Change c = changes.get(psId.getParentKey());
+ PatchSet ps = patchSets.get(psId);
+ if (c != null && ps != null) {
+ ChangeControl ctl = projectCtl.controlFor(c);
+ return ctl.isVisible(dbProvider.get())
+ && ctl.isPatchVisible(ps, dbProvider.get());
+ }
+ return false;
+ }
+
+ private void seedQueue(RevisionResource rsrc, RevWalk rw,
+ RevFlag seenCommit, Map<PatchSet.Id, PatchSet> patchSets,
+ LinkedList<String> q) throws IOException {
+ RevCommit tip = rw.parseCommit(ObjectId.fromString(
+ rsrc.getPatchSet().getRevision().get()));
+ tip.add(seenCommit);
+ q.add(tip.name());
+
+ Change.Id cId = rsrc.getChange().getId();
+ for (PatchSet p : patchSets.values()) {
+ if (cId.equals(p.getId().getParentKey())) {
+ try {
+ RevCommit c = rw.parseCommit(ObjectId.fromString(
+ rsrc.getPatchSet().getRevision().get()));
+ if (!c.has(seenCommit)) {
+ c.add(seenCommit);
+ q.add(c.name());
+ }
+ } catch (IOException e) {
+ log.warn(String.format(
+ "Cannot read patch set %d of %d",
+ p.getPatchSetId(), cId.get()), e);
+ }
+ }
+ }
+ }
+
+ private Multimap<String, PatchSet.Id> allChildren(Collection<Change.Id> ids)
+ throws OrmException {
+ ReviewDb db = dbProvider.get();
+ List<ResultSet<PatchSetAncestor>> t =
+ Lists.newArrayListWithCapacity(ids.size());
+ for (Change.Id id : ids) {
+ t.add(db.patchSetAncestors().byChange(id));
+ }
+
+ Multimap<String, PatchSet.Id> r = ArrayListMultimap.create();
+ for (ResultSet<PatchSetAncestor> rs : t) {
+ for (PatchSetAncestor a : rs) {
+ r.put(a.getAncestorRevision().get(), a.getPatchSet());
+ }
+ }
+ return r;
+ }
+
+ private static GitPerson toGitPerson(PersonIdent id) {
+ GitPerson p = new GitPerson();
+ p.name = id.getName();
+ p.email = id.getEmailAddress();
+ p.date = new Timestamp(id.getWhen().getTime());
+ p.tz = id.getTimeZoneOffset();
+ return p;
+ }
+
+ static class RelatedInfo {
+ List<ChangeAndCommit> changes;
+ }
+
+ static class ChangeAndCommit {
+ String changeId;
+ CommitInfo commit;
+ Integer _changeNumber;
+ Integer _revisionNumber;
+
+ ChangeAndCommit(@Nullable Change change, @Nullable PatchSet ps, RevCommit c) {
+ if (change != null) {
+ changeId = change.getKey().get();
+ _changeNumber = change.getChangeId();
+ _revisionNumber = ps != null ? ps.getPatchSetId() : null;
+ }
+
+ commit = new CommitInfo();
+ commit.commit = c.name();
+ commit.parents = Lists.newArrayListWithCapacity(c.getParentCount());
+ for (int i = 0; i < c.getParentCount(); i++) {
+ CommitInfo p = new CommitInfo();
+ p.commit = c.getParent(i).name();
+ commit.parents.add(p);
+ }
+ commit.author = toGitPerson(c.getAuthorIdent());
+ commit.subject = c.getShortMessage();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
new file mode 100644
index 0000000..b02f6f0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -0,0 +1,159 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.data.IncludedInDetail;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Resolve in which tags and branches a commit is included.
+ */
+public class IncludedInResolver {
+
+ private static final Logger log = LoggerFactory
+ .getLogger(IncludedInResolver.class);
+
+ public static IncludedInDetail resolve(final Repository repo,
+ final RevWalk rw, final RevCommit commit) throws IOException {
+
+ Set<Ref> tags =
+ new HashSet<Ref>(repo.getRefDatabase().getRefs(Constants.R_TAGS)
+ .values());
+ Set<Ref> branches =
+ new HashSet<Ref>(repo.getRefDatabase().getRefs(Constants.R_HEADS)
+ .values());
+ Set<Ref> allTagsAndBranches = new HashSet<Ref>();
+ allTagsAndBranches.addAll(tags);
+ allTagsAndBranches.addAll(branches);
+ Set<Ref> allMatchingTagsAndBranches =
+ includedIn(repo, rw, commit, allTagsAndBranches);
+
+ IncludedInDetail detail = new IncludedInDetail();
+ detail
+ .setBranches(getMatchingRefNames(allMatchingTagsAndBranches, branches));
+ detail.setTags(getMatchingRefNames(allMatchingTagsAndBranches, tags));
+
+ return detail;
+ }
+
+ /**
+ * Resolves which tip refs include the target commit.
+ */
+ private static Set<Ref> includedIn(final Repository repo, final RevWalk rw,
+ final RevCommit target, final Set<Ref> tipRefs) throws IOException,
+ MissingObjectException, IncorrectObjectTypeException {
+
+ Set<Ref> result = new HashSet<Ref>();
+
+ Map<RevCommit, Set<Ref>> tipsAndCommits = parseCommits(repo, rw, tipRefs);
+
+ List<RevCommit> tips = new ArrayList<RevCommit>(tipsAndCommits.keySet());
+ Collections.sort(tips, new Comparator<RevCommit>() {
+ @Override
+ public int compare(RevCommit c1, RevCommit c2) {
+ return c1.getCommitTime() - c2.getCommitTime();
+ }
+ });
+
+ Set<RevCommit> targetReachableFrom = new HashSet<RevCommit>();
+ targetReachableFrom.add(target);
+
+ for (RevCommit tip : tips) {
+ boolean commitFound = false;
+ rw.resetRetain(RevFlag.UNINTERESTING);
+ rw.markStart(tip);
+ for (RevCommit commit : rw) {
+ if (targetReachableFrom.contains(commit)) {
+ commitFound = true;
+ targetReachableFrom.add(tip);
+ result.addAll(tipsAndCommits.get(tip));
+ break;
+ }
+ }
+ if (!commitFound) {
+ rw.markUninteresting(tip);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the short names of refs which are as well in the matchingRefs list
+ * as well as in the allRef list.
+ */
+ private static List<String> getMatchingRefNames(Set<Ref> matchingRefs,
+ Set<Ref> allRefs) {
+ List<String> refNames = new ArrayList<String>();
+ for (Ref matchingRef : matchingRefs) {
+ if (allRefs.contains(matchingRef)) {
+ refNames.add(Repository.shortenRefName(matchingRef.getName()));
+ }
+ }
+ return refNames;
+ }
+
+ /**
+ * Parse commit of ref and store the relation between ref and commit.
+ */
+ private static Map<RevCommit, Set<Ref>> parseCommits(final Repository repo,
+ final RevWalk rw, final Set<Ref> refs) throws IOException {
+ Map<RevCommit, Set<Ref>> result = new HashMap<RevCommit, Set<Ref>>();
+ for (Ref ref : refs) {
+ final RevCommit commit;
+ try {
+ commit = rw.parseCommit(ref.getObjectId());
+ } catch (IncorrectObjectTypeException notCommit) {
+ // Its OK for a tag reference to point to a blob or a tree, this
+ // is common in the Linux kernel or git.git repository.
+ //
+ continue;
+ } catch (MissingObjectException notHere) {
+ // Log the problem with this branch, but keep processing.
+ //
+ log.warn("Reference " + ref.getName() + " in " + repo.getDirectory()
+ + " points to dangling object " + ref.getObjectId());
+ continue;
+ }
+ Set<Ref> relatedRefs = result.get(commit);
+ if (relatedRefs == null) {
+ relatedRefs = new HashSet<Ref>();
+ result.put(commit, relatedRefs);
+ }
+ relatedRefs.add(ref);
+ }
+ return result;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
new file mode 100644
index 0000000..4cec972
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.change.Index.Input;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.inject.Inject;
+
+import java.util.concurrent.ExecutionException;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+public class Index implements RestModifyView<ChangeResource, Input> {
+ public static class Input {
+ }
+
+ private final ChangeIndexer indexer;
+
+ @Inject
+ Index(ChangeIndexer indexer) {
+ this.indexer = indexer;
+ }
+
+ @Override
+ public Object apply(ChangeResource rsrc, Input input)
+ throws InterruptedException, ExecutionException {
+ indexer.index(rsrc.getChange()).get();
+ return Response.none();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
index 47863ac..97c7694 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
@@ -18,6 +18,7 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import com.google.gerrit.common.changes.Side;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -25,8 +26,6 @@
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountInfo;
-import com.google.gerrit.server.change.CommentInfo;
-import com.google.gerrit.server.change.CommentInfo.Side;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
new file mode 100644
index 0000000..c773735
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -0,0 +1,213 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.SubmitStrategyFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+public class Mergeable implements RestReadView<RevisionResource> {
+ private static final Logger log = LoggerFactory.getLogger(Mergeable.class);
+
+ public static class MergeableInfo {
+ public Project.SubmitType submitType;
+ public boolean mergeable;
+ }
+
+ private final TestSubmitType.Get submitType;
+ private final GitRepositoryManager gitManager;
+ private final SubmitStrategyFactory submitStrategyFactory;
+ private final Provider<ReviewDb> db;
+
+ @Inject
+ Mergeable(TestSubmitType.Get submitType,
+ GitRepositoryManager gitManager,
+ SubmitStrategyFactory submitStrategyFactory,
+ Provider<ReviewDb> db) {
+ this.submitType = submitType;
+ this.gitManager = gitManager;
+ this.submitStrategyFactory = submitStrategyFactory;
+ this.db = db;
+ }
+
+ @Override
+ public MergeableInfo apply(RevisionResource resource)
+ throws ResourceConflictException, BadRequestException, AuthException,
+ OrmException, RepositoryNotFoundException, IOException {
+ Change change = resource.getChange();
+ PatchSet ps = resource.getPatchSet();
+ MergeableInfo result = new MergeableInfo();
+
+ if (!change.getStatus().isOpen()) {
+ throw new ResourceConflictException("change is " + Submit.status(change));
+ } else if (!ps.getId().equals(change.currentPatchSetId())) {
+ // Only the current revision is mergeable. Others always fail.
+ return result;
+ }
+
+ result.submitType = submitType.apply(resource);
+ result.mergeable = change.isMergeable();
+
+ Repository git = gitManager.openRepository(change.getProject());
+ try {
+ Map<String, Ref> refs = git.getAllRefs();
+ Ref ref = refs.get(change.getDest().get());
+ if (isStale(change, ref)) {
+ result.mergeable =
+ refresh(change, ps, result.submitType, git, refs, ref);
+ }
+ } finally {
+ git.close();
+ }
+ return result;
+ }
+
+ private static boolean isStale(Change change, Ref ref) {
+ return change.getLastSha1MergeTested() == null
+ || !toRevId(ref).equals(change.getLastSha1MergeTested());
+ }
+
+ private static RevId toRevId(Ref ref) {
+ return new RevId(ref != null && ref.getObjectId() != null
+ ? ref.getObjectId().name()
+ : "");
+ }
+
+ private boolean refresh(Change change,
+ PatchSet ps,
+ Project.SubmitType type,
+ Repository git,
+ Map<String, Ref> refs,
+ Ref ref) throws IOException, OrmException {
+ RevWalk rw = new RevWalk(git) {
+ @Override
+ protected CodeReviewCommit createCommit(AnyObjectId id) {
+ return new CodeReviewCommit(id);
+ }
+ };
+ try {
+ ObjectId id;
+ try {
+ id = ObjectId.fromString(ps.getRevision().get());
+ } catch (IllegalArgumentException e) {
+ log.error(String.format(
+ "Invalid revision on patch set %d of %d",
+ ps.getId().get(),
+ change.getId().get()));
+ return false;
+ }
+
+ RevFlag canMerge = rw.newFlag("CAN_MERGE");
+ CodeReviewCommit rev = parse(rw, id);
+ rev.add(canMerge);
+
+ boolean mergeable;
+ if (ref == null || ref.getObjectId() == null) {
+ mergeable = true; // Assume yes on new branch.
+ } else {
+ CodeReviewCommit tip = parse(rw, ref.getObjectId());
+ Set<RevCommit> accepted = alreadyAccepted(rw, refs.values());
+ accepted.add(tip);
+ accepted.addAll(Arrays.asList(rev.getParents()));
+ mergeable = submitStrategyFactory.create(
+ type,
+ db.get(),
+ git,
+ rw,
+ null /*inserter*/,
+ canMerge,
+ accepted,
+ change.getDest()).dryRun(tip, rev);
+ }
+
+ Change c = db.get().changes().get(change.getId());
+ if (c != null) {
+ c.setMergeable(mergeable);
+ c.setLastSha1MergeTested(toRevId(ref));
+ db.get().changes().update(Collections.singleton(c));
+ }
+ return mergeable;
+ } catch (MergeException e) {
+ return false;
+ } catch (IOException e) {
+ log.error(String.format(
+ "Cannot merge test change %d", change.getId().get()), e);
+ return false;
+ } catch (NoSuchProjectException e) {
+ log.error(String.format(
+ "Cannot merge test change %d", change.getId().get()), e);
+ return false;
+ } finally {
+ rw.release();
+ }
+ }
+
+ private static Set<RevCommit> alreadyAccepted(RevWalk rw, Collection<Ref> refs)
+ throws MissingObjectException, IOException {
+ Set<RevCommit> accepted = Sets.newHashSet();
+ for (Ref r : refs) {
+ if (r.getName().startsWith(Constants.R_HEADS)
+ || r.getName().startsWith(Constants.R_TAGS)) {
+ try {
+ accepted.add(rw.parseCommit(r.getObjectId()));
+ } catch (IncorrectObjectTypeException nonCommit) {
+ // Not a commit? Skip over it.
+ }
+ }
+ }
+ return accepted;
+ }
+
+ private static CodeReviewCommit parse(RevWalk rw, ObjectId id)
+ throws MissingObjectException, IncorrectObjectTypeException, IOException {
+ return (CodeReviewCommit) rw.parseCommit(id);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index 690faf3..d292880 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -17,7 +17,7 @@
import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
import static com.google.gerrit.server.change.CommentResource.COMMENT_KIND;
import static com.google.gerrit.server.change.DraftResource.DRAFT_KIND;
-import static com.google.gerrit.server.change.PatchResource.PATCH_KIND;
+import static com.google.gerrit.server.change.FileResource.FILE_KIND;
import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
@@ -35,12 +35,12 @@
bind(Reviewers.class);
bind(Drafts.class);
bind(Comments.class);
- bind(Patches.class);
+ bind(Files.class);
DynamicMap.mapOf(binder(), CHANGE_KIND);
DynamicMap.mapOf(binder(), COMMENT_KIND);
DynamicMap.mapOf(binder(), DRAFT_KIND);
- DynamicMap.mapOf(binder(), PATCH_KIND);
+ DynamicMap.mapOf(binder(), FILE_KIND);
DynamicMap.mapOf(binder(), REVIEWER_KIND);
DynamicMap.mapOf(binder(), REVISION_KIND);
@@ -49,10 +49,15 @@
get(CHANGE_KIND, "topic").to(GetTopic.class);
put(CHANGE_KIND, "topic").to(PutTopic.class);
delete(CHANGE_KIND, "topic").to(PutTopic.class);
+ delete(CHANGE_KIND).to(DeleteDraftChange.class);
+ post(CHANGE_KIND, "delete").to(DeleteDraftChange.Action.class);
post(CHANGE_KIND, "abandon").to(Abandon.class);
+ post(CHANGE_KIND, "publish").to(Publish.CurrentRevision.class);
post(CHANGE_KIND, "restore").to(Restore.class);
post(CHANGE_KIND, "revert").to(Revert.class);
post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class);
+ post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class);
+ post(CHANGE_KIND, "index").to(Index.class);
post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
child(CHANGE_KIND, "reviewers").to(Reviewers.class);
@@ -60,9 +65,19 @@
delete(REVIEWER_KIND).to(DeleteReviewer.class);
child(CHANGE_KIND, "revisions").to(Revisions.class);
+ post(REVISION_KIND, "cherrypick").to(CherryPick.class);
+ get(REVISION_KIND, "commit").to(GetCommit.class);
+ delete(REVISION_KIND).to(DeleteDraftPatchSet.class);
+ post(REVISION_KIND, "delete").to(DeleteDraftPatchSet.Action.class);
+ get(REVISION_KIND, "mergeable").to(Mergeable.class);
+ post(REVISION_KIND, "publish").to(Publish.class);
+ get(REVISION_KIND, "related").to(GetRelated.class);
get(REVISION_KIND, "review").to(GetReview.class);
post(REVISION_KIND, "review").to(PostReview.class);
post(REVISION_KIND, "submit").to(Submit.class);
+ post(REVISION_KIND, "rebase").to(Rebase.class);
+ post(REVISION_KIND, "message").to(EditMessage.class);
+ get(REVISION_KIND, "patch").to(GetPatch.class);
get(REVISION_KIND, "submit_type").to(TestSubmitType.Get.class);
post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class);
post(REVISION_KIND, "test.submit_type").to(TestSubmitType.class);
@@ -76,9 +91,11 @@
child(REVISION_KIND, "comments").to(Comments.class);
get(COMMENT_KIND).to(GetComment.class);
- child(REVISION_KIND, "files").to(Patches.class);
- put(PATCH_KIND, "reviewed").to(PutReviewed.class);
- delete(PATCH_KIND, "reviewed").to(DeleteReviewed.class);
+ child(REVISION_KIND, "files").to(Files.class);
+ put(FILE_KIND, "reviewed").to(PutReviewed.class);
+ delete(FILE_KIND, "reviewed").to(DeleteReviewed.class);
+ get(FILE_KIND, "content").to(GetContent.class);
+ get(FILE_KIND, "diff").to(GetDiff.class);
install(new FactoryModule() {
@Override
@@ -86,6 +103,8 @@
factory(ReviewerResource.Factory.class);
factory(AccountInfo.Loader.Factory.class);
factory(EmailReviewComments.Factory.class);
+ factory(ChangeInserter.Factory.class);
+ factory(PatchSetInserter.Factory.class);
}
});
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchResource.java
deleted file mode 100644
index d3cf5c6..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchResource.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.gerrit.extensions.restapi.RestResource;
-import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.inject.TypeLiteral;
-
-public class PatchResource implements RestResource {
- public static final TypeLiteral<RestView<PatchResource>> PATCH_KIND =
- new TypeLiteral<RestView<PatchResource>>() {};
-
- private final RevisionResource rev;
- private final Patch.Key key;
-
- PatchResource(RevisionResource rev, String name) {
- this.rev = rev;
- this.key = new Patch.Key(rev.getPatchSet().getId(), name);
- }
-
- public Patch.Key getPatchKey() {
- return key;
- }
-
- Account.Id getAccountId() {
- return rev.getAccountId();
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
new file mode 100644
index 0000000..5e2c797
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -0,0 +1,356 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.mail.ReplacePatchSetSender;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.ssh.NoSshInfo;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class PatchSetInserter {
+ private static final Logger log =
+ LoggerFactory.getLogger(PatchSetInserter.class);
+
+ public static interface Factory {
+ PatchSetInserter create(Repository git, RevWalk revWalk, RefControl refControl,
+ IdentifiedUser user, Change change, RevCommit commit);
+ }
+
+ /**
+ * Whether to use {@link CommitValidators#validateForGerritCommits},
+ * {@link CommitValidators#validateForReceiveCommits}, or no commit
+ * validation.
+ */
+ public static enum ValidatePolicy {
+ GERRIT, RECEIVE_COMMITS, NONE;
+ }
+
+ private final ChangeHooks hooks;
+ private final TrackingFooters trackingFooters;
+ private final PatchSetInfoFactory patchSetInfoFactory;
+ private final ReviewDb db;
+ private final IdentifiedUser user;
+ private final GitReferenceUpdated gitRefUpdated;
+ private final CommitValidators.Factory commitValidatorsFactory;
+ private final ChangeIndexer indexer;
+ private final ReplacePatchSetSender.Factory replacePatchSetFactory;
+
+ private final Repository git;
+ private final RevWalk revWalk;
+ private final RevCommit commit;
+ private final Change change;
+ private final RefControl refControl;
+
+ private PatchSet patchSet;
+ private ChangeMessage changeMessage;
+ private boolean copyLabels;
+ private SshInfo sshInfo;
+ private ValidatePolicy validatePolicy = ValidatePolicy.GERRIT;
+ private boolean draft;
+ private boolean runHooks;
+ private boolean sendMail;
+
+ @Inject
+ public PatchSetInserter(ChangeHooks hooks,
+ TrackingFooters trackingFooters,
+ ReviewDb db,
+ PatchSetInfoFactory patchSetInfoFactory,
+ GitReferenceUpdated gitRefUpdated,
+ CommitValidators.Factory commitValidatorsFactory,
+ ChangeIndexer indexer,
+ ReplacePatchSetSender.Factory replacePatchSetFactory,
+ @Assisted Repository git,
+ @Assisted RevWalk revWalk,
+ @Assisted RefControl refControl,
+ @Assisted IdentifiedUser user,
+ @Assisted Change change,
+ @Assisted RevCommit commit) {
+ this.hooks = hooks;
+ this.trackingFooters = trackingFooters;
+ this.db = db;
+ this.patchSetInfoFactory = patchSetInfoFactory;
+ this.user = user;
+ this.gitRefUpdated = gitRefUpdated;
+ this.commitValidatorsFactory = commitValidatorsFactory;
+ this.indexer = indexer;
+ this.replacePatchSetFactory = replacePatchSetFactory;
+
+ this.git = git;
+ this.revWalk = revWalk;
+ this.refControl = refControl;
+ this.change = change;
+ this.commit = commit;
+ this.runHooks = true;
+ this.sendMail = true;
+ }
+
+ public PatchSetInserter setPatchSet(PatchSet patchSet) {
+ PatchSet.Id psid = patchSet.getId();
+ checkArgument(psid.getParentKey().equals(change.getId()),
+ "patch set %s not for change %s", psid, change.getId());
+ checkArgument(psid.get() > change.currentPatchSetId().get(),
+ "new patch set ID %s is not greater than current patch set ID %s",
+ psid.get(), change.currentPatchSetId().get());
+ this.patchSet = patchSet;
+ return this;
+ }
+
+ public PatchSet.Id getPatchSetId() {
+ init();
+ return patchSet.getId();
+ }
+
+ public PatchSetInserter setMessage(String message) throws OrmException {
+ changeMessage = new ChangeMessage(new ChangeMessage.Key(change.getId(),
+ ChangeUtil.messageUUID(db)), user.getAccountId(), patchSet.getId());
+ changeMessage.setMessage(message);
+ return this;
+ }
+
+ public PatchSetInserter setMessage(ChangeMessage changeMessage) throws OrmException {
+ this.changeMessage = changeMessage;
+ return this;
+ }
+
+ public PatchSetInserter setCopyLabels(boolean copyLabels) {
+ this.copyLabels = copyLabels;
+ return this;
+ }
+
+ public PatchSetInserter setSshInfo(SshInfo sshInfo) {
+ this.sshInfo = sshInfo;
+ return this;
+ }
+
+ public PatchSetInserter setValidatePolicy(ValidatePolicy validate) {
+ this.validatePolicy = checkNotNull(validate);
+ return this;
+ }
+
+ public PatchSetInserter setDraft(boolean draft) {
+ this.draft = draft;
+ return this;
+ }
+
+ public PatchSetInserter setRunHooks(boolean runHooks) {
+ this.runHooks = runHooks;
+ return this;
+ }
+
+ public PatchSetInserter setSendMail(boolean sendMail) {
+ this.sendMail = sendMail;
+ return this;
+ }
+
+ public Change insert() throws InvalidChangeOperationException, OrmException,
+ IOException {
+ init();
+ validate();
+
+ Change updatedChange;
+ RefUpdate ru = git.updateRef(patchSet.getRefName());
+ ru.setExpectedOldObjectId(ObjectId.zeroId());
+ ru.setNewObjectId(commit);
+ ru.disableRefLog();
+ if (ru.update(revWalk) != RefUpdate.Result.NEW) {
+ throw new IOException(String.format(
+ "Failed to create ref %s in %s: %s", patchSet.getRefName(),
+ change.getDest().getParentKey().get(), ru.getResult()));
+ }
+ gitRefUpdated.fire(change.getProject(), ru);
+
+ final PatchSet.Id currentPatchSetId = change.currentPatchSetId();
+
+ db.changes().beginTransaction(change.getId());
+ try {
+ if (!db.changes().get(change.getId()).getStatus().isOpen()) {
+ throw new InvalidChangeOperationException(String.format(
+ "Change %s is closed", change.getId()));
+ }
+
+ ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
+ db.patchSets().insert(Collections.singleton(patchSet));
+
+ final List<PatchSetApproval> oldPatchSetApprovals =
+ db.patchSetApprovals().byChange(change.getId()).toList();
+ final Set<Account.Id> oldReviewers = Sets.newHashSet();
+ final Set<Account.Id> oldCC = Sets.newHashSet();
+ for (PatchSetApproval a : oldPatchSetApprovals) {
+ if (a.getValue() != 0) {
+ oldReviewers.add(a.getAccountId());
+ } else {
+ oldCC.add(a.getAccountId());
+ }
+ }
+
+ updatedChange =
+ db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus().isClosed()) {
+ return null;
+ }
+ if (!change.currentPatchSetId().equals(currentPatchSetId)) {
+ return null;
+ }
+ if (change.getStatus() != Change.Status.DRAFT) {
+ change.setStatus(Change.Status.NEW);
+ }
+ change.setLastSha1MergeTested(null);
+ change.setCurrentPatchSet(patchSetInfoFactory.get(commit,
+ patchSet.getId()));
+ ChangeUtil.updated(change);
+ return change;
+ }
+ });
+ if (updatedChange == null) {
+ throw new ChangeModifiedException(String.format(
+ "Change %s was modified", change.getId()));
+ }
+
+ if (copyLabels) {
+ ApprovalsUtil.copyLabels(db, refControl.getProjectControl()
+ .getLabelTypes(), currentPatchSetId, updatedChange.currentPatchSetId());
+ }
+
+ final List<FooterLine> footerLines = commit.getFooterLines();
+ ChangeUtil.updateTrackingIds(db, updatedChange, trackingFooters, footerLines);
+ db.commit();
+
+ if (changeMessage != null) {
+ db.changeMessages().insert(Collections.singleton(changeMessage));
+ }
+
+ if (sendMail) {
+ try {
+ PatchSetInfo info = patchSetInfoFactory.get(commit, patchSet.getId());
+ ReplacePatchSetSender cm =
+ replacePatchSetFactory.create(updatedChange);
+ cm.setFrom(user.getAccountId());
+ cm.setPatchSet(patchSet, info);
+ cm.setChangeMessage(changeMessage);
+ cm.addReviewers(oldReviewers);
+ cm.addExtraCC(oldCC);
+ cm.send();
+ } catch (Exception err) {
+ log.error("Cannot send email for new patch set on change " + updatedChange.getId(),
+ err);
+ }
+ }
+
+ indexer.index(updatedChange);
+ if (runHooks) {
+ hooks.doPatchsetCreatedHook(updatedChange, patchSet, db);
+ }
+ } finally {
+ db.rollback();
+ }
+ return updatedChange;
+ }
+
+ private void init() {
+ if (sshInfo == null) {
+ sshInfo = new NoSshInfo();
+ }
+ if (patchSet == null) {
+ patchSet = new PatchSet(
+ ChangeUtil.nextPatchSetId(git, change.currentPatchSetId()));
+ patchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
+ patchSet.setUploader(change.getOwner());
+ patchSet.setRevision(new RevId(commit.name()));
+ }
+ patchSet.setDraft(draft);
+ }
+
+ private void validate() throws InvalidChangeOperationException {
+ CommitValidators cv = commitValidatorsFactory.create(refControl, sshInfo, git);
+
+ String refName = patchSet.getRefName();
+ CommitReceivedEvent event = new CommitReceivedEvent(
+ new ReceiveCommand(
+ ObjectId.zeroId(),
+ commit.getId(),
+ refName.substring(0, refName.lastIndexOf('/') + 1) + "new"),
+ refControl.getProjectControl().getProject(), refControl.getRefName(),
+ commit, user);
+
+ try {
+ switch (validatePolicy) {
+ case RECEIVE_COMMITS:
+ cv.validateForReceiveCommits(event);
+ break;
+ case GERRIT:
+ cv.validateForGerritCommits(event);
+ break;
+ case NONE:
+ break;
+ }
+ } catch (CommitValidationException e) {
+ throw new InvalidChangeOperationException(e.getMessage());
+ }
+ }
+
+ public class ChangeModifiedException extends InvalidChangeOperationException {
+ private static final long serialVersionUID = 1L;
+
+ public ChangeModifiedException(String msg) {
+ super(msg);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Patches.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Patches.java
deleted file mode 100644
index cb0a9bf..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Patches.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.ChildCollection;
-import com.google.gerrit.extensions.restapi.IdString;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-
-class Patches implements ChildCollection<RevisionResource, PatchResource> {
- private final DynamicMap<RestView<PatchResource>> views;
-
- @Inject
- Patches(DynamicMap<RestView<PatchResource>> views) {
- this.views = views;
- }
-
- @Override
- public DynamicMap<RestView<PatchResource>> views() {
- return views;
- }
-
- @Override
- public RestView<RevisionResource> list() throws AuthException {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public PatchResource parse(RevisionResource rev, IdString id)
- throws ResourceNotFoundException, OrmException, AuthException {
- return new PatchResource(rev, id.get());
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 9c14bcb..91c6abe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -20,7 +20,9 @@
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.ListenableFuture;
import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.changes.Side;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.Permission;
@@ -29,16 +31,20 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.CommentRange;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountsCollection;
import com.google.gerrit.server.change.PostReview.Input;
+import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -51,6 +57,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.ExecutionException;
public class PostReview implements RestModifyView<RevisionResource, Input> {
private static final Logger log = LoggerFactory.getLogger(PostReview.class);
@@ -80,6 +87,20 @@
/** Who to send email notifications to after review is stored. */
public NotifyHandling notify = NotifyHandling.ALL;
+
+ /**
+ * Account ID, name, email address or username of another user. The review
+ * will be posted/updated on behalf of this named user instead of the
+ * caller. Caller must have the labelAs-$NAME permission granted for each
+ * label that appears in {@link #labels}. This is in addition to the named
+ * user also needing to have permission to use the labels.
+ * <p>
+ * {@link #strictLabels} impacts how labels is processed for the named user,
+ * not the caller.
+ */
+ public String onBehalfOf;
+
+ public boolean waitForCommit;
}
public static enum DraftHandling {
@@ -92,10 +113,11 @@
static class Comment {
String id;
- CommentInfo.Side side;
+ Side side;
int line;
String inReplyTo;
String message;
+ CommentRange range;
}
static class Output {
@@ -103,6 +125,8 @@
}
private final ReviewDb db;
+ private final ChangeIndexer indexer;
+ private final AccountsCollection accounts;
private final EmailReviewComments.Factory email;
@Deprecated private final ChangeHooks hooks;
@@ -115,16 +139,24 @@
@Inject
PostReview(ReviewDb db,
+ ChangeIndexer indexer,
+ AccountsCollection accounts,
EmailReviewComments.Factory email,
ChangeHooks hooks) {
this.db = db;
+ this.indexer = indexer;
+ this.accounts = accounts;
this.email = email;
this.hooks = hooks;
}
@Override
public Object apply(RevisionResource revision, Input input)
- throws AuthException, BadRequestException, OrmException {
+ throws AuthException, BadRequestException, OrmException,
+ UnprocessableEntityException, InterruptedException, ExecutionException {
+ if (input.onBehalfOf != null) {
+ revision = onBehalfOf(revision, input);
+ }
if (input.labels != null) {
checkLabels(revision, input.strictLabels, input.labels);
}
@@ -149,6 +181,11 @@
if (dirty) {
db.changes().update(Collections.singleton(change));
db.commit();
+
+ ListenableFuture<?> indexWrite = indexer.index(change);
+ if (input.waitForCommit) {
+ indexWrite.get();
+ }
}
} finally {
db.rollback();
@@ -170,6 +207,45 @@
return output;
}
+ private RevisionResource onBehalfOf(RevisionResource rev, Input in)
+ throws BadRequestException, AuthException, UnprocessableEntityException,
+ OrmException {
+ if (in.labels == null || in.labels.isEmpty()) {
+ throw new AuthException(String.format(
+ "label required to post review on behalf of \"%s\"",
+ in.onBehalfOf));
+ }
+
+ ChangeControl caller = rev.getControl();
+ Iterator<Map.Entry<String, Short>> itr = in.labels.entrySet().iterator();
+ while (itr.hasNext()) {
+ Map.Entry<String, Short> ent = itr.next();
+ LabelType type = caller.getLabelTypes().byLabel(ent.getKey());
+ if (type == null && in.strictLabels) {
+ throw new BadRequestException(String.format(
+ "label \"%s\" is not a configured label", ent.getKey()));
+ } else if (type == null) {
+ itr.remove();
+ continue;
+ }
+
+ PermissionRange r = caller.getRange(Permission.forLabelAs(type.getName()));
+ if (r == null || r.isEmpty() || !r.contains(ent.getValue())) {
+ throw new AuthException(String.format(
+ "not permitted to modify label \"%s\" on behalf of \"%s\"",
+ ent.getKey(), in.onBehalfOf));
+ }
+ }
+ if (in.labels.isEmpty()) {
+ throw new AuthException(String.format(
+ "label required to post review on behalf of \"%s\"",
+ in.onBehalfOf));
+ }
+
+ ChangeControl target = caller.forUser(accounts.parse(in.onBehalfOf));
+ return new RevisionResource(new ChangeResource(target), rev.getPatchSet());
+ }
+
private void checkLabels(RevisionResource revision, boolean strict,
Map<String, Short> labels) throws BadRequestException, AuthException {
ChangeControl ctl = revision.getControl();
@@ -289,8 +365,9 @@
}
e.setStatus(PatchLineComment.Status.PUBLISHED);
e.setWrittenOn(timestamp);
- e.setSide(c.side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
+ e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
e.setMessage(c.message);
+ e.setRange(c.range);
(create ? ins : upd).add(e);
}
}
@@ -350,11 +427,12 @@
}
PatchSetApproval c = current.remove(name);
+ String normName = lt.getName();
if (ent.getValue() == null || ent.getValue() == 0) {
// User requested delete of this label.
if (c != null) {
if (c.getValue() != 0) {
- labelDelta.add("-" + name);
+ labelDelta.add("-" + normName);
}
del.add(c);
}
@@ -363,10 +441,10 @@
c.setGranted(timestamp);
c.cache(change);
upd.add(c);
- labelDelta.add(format(name, c.getValue()));
- categories.put(name, c.getValue());
+ labelDelta.add(format(normName, c.getValue()));
+ categories.put(normName, c.getValue());
} else if (c != null && c.getValue() == ent.getValue()) {
- current.put(name, c);
+ current.put(normName, c);
} else if (c == null) {
c = new PatchSetApproval(new PatchSetApproval.Key(
rsrc.getPatchSet().getId(),
@@ -376,8 +454,8 @@
c.setGranted(timestamp);
c.cache(change);
ins.add(c);
- labelDelta.add(format(name, c.getValue()));
- categories.put(name, c.getValue());
+ labelDelta.add(format(normName, c.getValue()));
+ categories.put(normName, c.getValue());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index 70cf259..f41d97d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -37,6 +37,7 @@
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountInfo;
@@ -47,6 +48,7 @@
import com.google.gerrit.server.change.ReviewerJson.ReviewerInfo;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.mail.AddReviewerSender;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchProjectException;
@@ -55,14 +57,20 @@
import com.google.inject.Provider;
import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.io.IOException;
import java.text.MessageFormat;
import java.util.List;
import java.util.Set;
public class PostReviewers implements RestModifyView<ChangeResource, Input> {
- public final static int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
- public final static int DEFAULT_MAX_REVIEWERS = 20;
+ private static final Logger log = LoggerFactory
+ .getLogger(PostReviewers.class);
+
+ public static final int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
+ public static final int DEFAULT_MAX_REVIEWERS = 20;
public static class Input {
@DefaultInput
@@ -80,13 +88,14 @@
private final Provider<GroupsCollection> groupsCollection;
private final GroupMembers.Factory groupMembersFactory;
private final AccountInfo.Loader.Factory accountLoaderFactory;
- private final Provider<ReviewDb> db;
+ private final Provider<ReviewDb> dbProvider;
private final IdentifiedUser currentUser;
private final IdentifiedUser.GenericFactory identifiedUserFactory;
private final Config cfg;
private final ChangeHooks hooks;
private final AccountCache accountCache;
private final ReviewerJson json;
+ private final ChangeIndexer indexer;
@Inject
PostReviewers(AccountsCollection accounts,
@@ -101,26 +110,28 @@
@GerritServerConfig Config cfg,
ChangeHooks hooks,
AccountCache accountCache,
- ReviewerJson json) {
+ ReviewerJson json,
+ ChangeIndexer indexer) {
this.accounts = accounts;
this.reviewerFactory = reviewerFactory;
this.addReviewerSenderFactory = addReviewerSenderFactory;
this.groupsCollection = groupsCollection;
this.groupMembersFactory = groupMembersFactory;
this.accountLoaderFactory = accountLoaderFactory;
- this.db = db;
+ this.dbProvider = db;
this.currentUser = currentUser;
this.identifiedUserFactory = identifiedUserFactory;
this.cfg = cfg;
this.hooks = hooks;
this.accountCache = accountCache;
this.json = json;
+ this.indexer = indexer;
}
@Override
public PostResult apply(ChangeResource rsrc, Input input)
throws BadRequestException, ResourceNotFoundException, AuthException,
- UnprocessableEntityException, OrmException, EmailException {
+ UnprocessableEntityException, OrmException, EmailException, IOException {
if (input.reviewer == null) {
throw new BadRequestException("missing reviewer field");
}
@@ -147,8 +158,8 @@
}
private PostResult putGroup(ChangeResource rsrc, Input input)
- throws ResourceNotFoundException, AuthException, BadRequestException,
- UnprocessableEntityException, OrmException, EmailException {
+ throws BadRequestException,
+ UnprocessableEntityException, OrmException, EmailException, IOException {
GroupDescription.Basic group = groupsCollection.get().parseInternal(input.reviewer);
PostResult result = new PostResult();
if (!isLegalReviewerGroup(group.getGroupUUID())) {
@@ -214,9 +225,10 @@
return;
}
+ ReviewDb db = dbProvider.get();
PatchSet.Id psid = rsrc.getChange().currentPatchSetId();
Set<Account.Id> existing = Sets.newHashSet();
- for (PatchSetApproval psa : db.get().patchSetApprovals().byPatchSet(psid)) {
+ for (PatchSetApproval psa : db.patchSetApprovals().byPatchSet(psid)) {
existing.add(psa.getAccountId());
}
@@ -234,7 +246,20 @@
new ReviewerInfo(id), control, ImmutableList.of(psa)));
toInsert.add(psa);
}
- db.get().patchSetApprovals().insert(toInsert);
+ if (toInsert.isEmpty()) {
+ return;
+ }
+
+ db.changes().beginTransaction(rsrc.getChange().getId());
+ try {
+ ChangeUtil.bumpRowVersionNotLastUpdatedOn(rsrc.getChange().getId(), db);
+ db.patchSetApprovals().insert(toInsert);
+ db.commit();
+ } finally {
+ db.rollback();
+ }
+
+ indexer.index(rsrc.getChange());
accountLoaderFactory.create(true).fill(result.reviewers);
postAdd(rsrc.getChange(), result);
}
@@ -247,10 +272,10 @@
// Execute hook for added reviewers
//
- PatchSet patchSet = db.get().patchSets().get(change.currentPatchSetId());
+ PatchSet patchSet = dbProvider.get().patchSets().get(change.currentPatchSetId());
for (AccountInfo info : result.reviewers) {
Account account = accountCache.get(info._id).getAccount();
- hooks.doReviewerAddedHook(change, account, patchSet, db.get());
+ hooks.doReviewerAddedHook(change, account, patchSet, dbProvider.get());
}
// Email the reviewers
@@ -264,12 +289,15 @@
}
}
if (!added.isEmpty()) {
- AddReviewerSender cm;
-
- cm = addReviewerSenderFactory.create(change);
- cm.setFrom(currentUser.getAccountId());
- cm.addReviewers(added);
- cm.send();
+ try {
+ AddReviewerSender cm = addReviewerSenderFactory.create(change);
+ cm.setFrom(currentUser.getAccountId());
+ cm.addReviewers(added);
+ cm.send();
+ } catch (Exception err) {
+ log.error("Cannot send email to new reviewers of change "
+ + change.getId(), err);
+ }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java
new file mode 100644
index 0000000..7355ef3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Publish.java
@@ -0,0 +1,161 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.mail.PatchSetNotificationSender;
+import com.google.gerrit.server.change.Publish.Input;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+
+public class Publish implements RestModifyView<RevisionResource, Input>,
+ UiAction<RevisionResource> {
+ public static class Input {
+ }
+
+ private final Provider<ReviewDb> dbProvider;
+ private final PatchSetNotificationSender sender;
+ private final ChangeHooks hooks;
+ private final ChangeIndexer indexer;
+
+ @Inject
+ public Publish(Provider<ReviewDb> dbProvider,
+ PatchSetNotificationSender sender,
+ ChangeHooks hooks,
+ ChangeIndexer indexer) {
+ this.dbProvider = dbProvider;
+ this.sender = sender;
+ this.hooks = hooks;
+ this.indexer = indexer;
+ }
+
+ @Override
+ public Object apply(RevisionResource rsrc, Input input) throws IOException,
+ ResourceNotFoundException, ResourceConflictException,
+ OrmException, AuthException {
+ if (!rsrc.getPatchSet().isDraft()) {
+ throw new ResourceConflictException("Patch set is not a draft");
+ }
+
+ if (!rsrc.getControl().canPublish(dbProvider.get())) {
+ throw new AuthException("Cannot publish this draft patch set");
+ }
+
+ PatchSet updatedPatchSet = updateDraftPatchSet(rsrc);
+ Change updatedChange = updateDraftChange(rsrc);
+
+ try {
+ if (!updatedPatchSet.isDraft()
+ || updatedChange.getStatus() == Change.Status.NEW) {
+ indexer.index(updatedChange);
+ hooks.doDraftPublishedHook(updatedChange, updatedPatchSet, dbProvider.get());
+ sender.send(rsrc.getChange().getStatus() == Change.Status.DRAFT,
+ rsrc.getUser(), updatedChange, updatedPatchSet,
+ rsrc.getControl().getLabelTypes());
+ }
+ } catch (PatchSetInfoNotAvailableException e) {
+ throw new ResourceNotFoundException(e.getMessage());
+ }
+
+ return Response.none();
+ }
+
+ private Change updateDraftChange(RevisionResource rsrc) throws OrmException {
+ Change updatedChange = dbProvider.get().changes()
+ .atomicUpdate(rsrc.getChange().getId(),
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus() == Change.Status.DRAFT) {
+ change.setStatus(Change.Status.NEW);
+ ChangeUtil.updated(change);
+ }
+ return change;
+ }
+ });
+ return updatedChange;
+ }
+
+ private PatchSet updateDraftPatchSet(RevisionResource rsrc) throws OrmException {
+ final PatchSet updatedPatchSet = dbProvider.get().patchSets()
+ .atomicUpdate(rsrc.getPatchSet().getId(),
+ new AtomicUpdate<PatchSet>() {
+ @Override
+ public PatchSet update(PatchSet patchset) {
+ patchset.setDraft(false);
+ return patchset;
+ }
+ });
+ return updatedPatchSet;
+ }
+
+ @Override
+ public UiAction.Description getDescription(RevisionResource rsrc) {
+ PatchSet.Id current = rsrc.getChange().currentPatchSetId();
+ try {
+ return new UiAction.Description()
+ .setTitle(String.format("Publish Revision %d",
+ rsrc.getPatchSet().getPatchSetId()))
+ .setVisible(rsrc.getPatchSet().isDraft()
+ && rsrc.getPatchSet().getId().equals(current)
+ && rsrc.getControl().canPublish(dbProvider.get()));
+ } catch (OrmException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public static class CurrentRevision implements
+ RestModifyView<ChangeResource, Input> {
+ private final Provider<ReviewDb> dbProvider;
+ private final Publish publish;
+
+ @Inject
+ CurrentRevision(Provider<ReviewDb> dbProvider,
+ Publish publish) {
+ this.dbProvider = dbProvider;
+ this.publish = publish;
+ }
+
+ @Override
+ public Object apply(ChangeResource rsrc, Input input) throws AuthException,
+ ResourceConflictException, ResourceConflictException, IOException,
+ OrmException, ResourceNotFoundException, AuthException {
+ PatchSet ps = dbProvider.get().patchSets()
+ .get(rsrc.getChange().currentPatchSetId());
+ if (ps == null) {
+ throw new ResourceConflictException("current revision is missing");
+ } else if (!rsrc.getControl().isPatchVisible(ps, dbProvider.get())) {
+ throw new AuthException("current revision not accessible");
+ }
+ return publish.apply(new RevisionResource(rsrc, ps), input);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
index befb8d7..005d493 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.change;
+import com.google.gerrit.common.changes.Side;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -22,8 +23,8 @@
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.CommentRange;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.CommentInfo.Side;
import com.google.gerrit.server.change.PutDraft.Input;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -41,6 +42,7 @@
Integer line;
String inReplyTo;
Timestamp updated; // Accepted but ignored.
+ CommentRange range;
@DefaultInput
String message;
@@ -67,6 +69,8 @@
throw new BadRequestException("id must match URL");
} else if (in.line != null && in.line < 0) {
throw new BadRequestException("line must be >= 0");
+ } else if (in.line != null && in.range != null && in.line != in.range.getEndLine()) {
+ throw new BadRequestException("range endLine must be on the same line as the comment");
}
if (in.path != null
@@ -90,15 +94,16 @@
private PatchLineComment update(PatchLineComment e, Input in) {
if (in.side != null) {
- e.setSide(in.side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
- }
- if (in.line != null) {
- e.setLine(in.line);
+ e.setSide(in.side == Side.PARENT ? (short) 0 : (short) 1);
}
if (in.inReplyTo != null) {
e.setParentUuid(Url.decode(in.inReplyTo));
}
e.setMessage(in.message.trim());
+ if (in.range != null || in.line != null) {
+ e.setRange(in.range);
+ e.setLine(in.range != null ? in.range.getEndLine() : in.line);
+ }
e.updated();
return e;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
index b96b480..5f26be2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -15,18 +15,21 @@
package com.google.gerrit.server.change;
import com.google.common.base.Strings;
+import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.DefaultInput;
-import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.PutTopic.Input;
+import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gwtorm.server.AtomicUpdate;
import com.google.inject.Inject;
@@ -34,8 +37,11 @@
import java.util.Collections;
-class PutTopic implements RestModifyView<ChangeResource, Input> {
+class PutTopic implements RestModifyView<ChangeResource, Input>,
+ UiAction<ChangeResource> {
private final Provider<ReviewDb> dbProvider;
+ private final ChangeIndexer indexer;
+ private final ChangeHooks hooks;
static class Input {
@DefaultInput
@@ -44,8 +50,11 @@
}
@Inject
- PutTopic(Provider<ReviewDb> dbProvider) {
+ PutTopic(Provider<ReviewDb> dbProvider, ChangeIndexer indexer,
+ ChangeHooks hooks) {
this.dbProvider = dbProvider;
+ this.indexer = indexer;
+ this.hooks = hooks;
}
@Override
@@ -77,9 +86,10 @@
oldTopicName, newTopicName);
}
+ IdentifiedUser currentUser = ((IdentifiedUser) control.getCurrentUser());
ChangeMessage cmsg = new ChangeMessage(
new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)),
- ((IdentifiedUser) control.getCurrentUser()).getAccountId(),
+ currentUser.getAccountId(),
change.currentPatchSetId());
StringBuilder msgBuf = new StringBuilder(summary);
if (!Strings.isNullOrEmpty(input.message)) {
@@ -88,18 +98,35 @@
}
cmsg.setMessage(msgBuf.toString());
- db.changes().atomicUpdate(change.getId(),
- new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- change.setTopic(Strings.emptyToNull(newTopicName));
- return change;
- }
- });
- db.changeMessages().insert(Collections.singleton(cmsg));
+ db.changes().beginTransaction(change.getId());
+ try {
+ change = db.changes().atomicUpdate(change.getId(),
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ change.setTopic(Strings.emptyToNull(newTopicName));
+ ChangeUtil.updated(change);
+ return change;
+ }
+ });
+ db.changeMessages().insert(Collections.singleton(cmsg));
+ db.commit();
+ } finally {
+ db.rollback();
+ }
+ indexer.index(change);
+ hooks.doTopicChangedHook(change, currentUser.getAccount(),
+ oldTopicName, db);
}
return Strings.isNullOrEmpty(newTopicName)
? Response.none()
: newTopicName;
}
+
+ @Override
+ public UiAction.Description getDescription(ChangeResource resource) {
+ return new UiAction.Description()
+ .setLabel("Edit Topic")
+ .setVisible(resource.getControl().canEditTopicName());
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
new file mode 100644
index 0000000..3831c20
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.change.Rebase.Input;
+import com.google.gerrit.server.changedetail.RebaseChange;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+
+public class Rebase implements RestModifyView<RevisionResource, Input>,
+ UiAction<RevisionResource> {
+ public static class Input {
+ }
+
+ private final Provider<RebaseChange> rebaseChange;
+ private final ChangeJson json;
+
+ @Inject
+ public Rebase(Provider<RebaseChange> rebaseChange, ChangeJson json) {
+ this.rebaseChange = rebaseChange;
+ this.json = json;
+ }
+
+ @Override
+ public ChangeInfo apply(RevisionResource rsrc, Input input)
+ throws AuthException, ResourceNotFoundException,
+ ResourceConflictException, EmailException, OrmException {
+ ChangeControl control = rsrc.getControl();
+ Change change = rsrc.getChange();
+ if (!control.canRebase()) {
+ throw new AuthException("rebase not permitted");
+ } else if (!change.getStatus().isOpen()) {
+ throw new ResourceConflictException("change is "
+ + change.getStatus().name().toLowerCase());
+ }
+
+ try {
+ rebaseChange.get().rebase(rsrc.getPatchSet().getId(), rsrc.getUser());
+ } catch (InvalidChangeOperationException e) {
+ throw new ResourceConflictException(e.getMessage());
+ } catch (IOException e) {
+ throw new ResourceConflictException(e.getMessage());
+ } catch (NoSuchChangeException e) {
+ throw new ResourceNotFoundException(change.getId().toString());
+ }
+
+ json.addOption(ListChangesOption.CURRENT_REVISION)
+ .addOption(ListChangesOption.CURRENT_COMMIT);
+ return json.format(change.getId());
+ }
+
+ @Override
+ public UiAction.Description getDescription(RevisionResource resource) {
+ return new UiAction.Description()
+ .setLabel("Rebase")
+ .setTitle("Rebase onto tip of branch or parent change")
+ .setVisible(resource.getChange().getStatus().isOpen()
+ && resource.getControl().canRebase()
+ && rebaseChange.get().canRebase(resource));
+ }
+
+ public static class CurrentRevision implements
+ RestModifyView<ChangeResource, Input> {
+ private final Provider<ReviewDb> dbProvider;
+ private final Rebase rebase;
+
+ @Inject
+ CurrentRevision(Provider<ReviewDb> dbProvider, Rebase rebase) {
+ this.dbProvider = dbProvider;
+ this.rebase = rebase;
+ }
+
+ @Override
+ public ChangeInfo apply(ChangeResource rsrc, Input input)
+ throws AuthException, ResourceNotFoundException,
+ ResourceConflictException, EmailException, OrmException {
+ PatchSet ps =
+ dbProvider.get().patchSets()
+ .get(rsrc.getChange().currentPatchSetId());
+ if (ps == null) {
+ throw new ResourceConflictException("current revision is missing");
+ } else if (!rsrc.getControl().isPatchVisible(ps, dbProvider.get())) {
+ throw new AuthException("current revision not accessible");
+ }
+ return rebase.apply(new RevisionResource(rsrc, ps), input);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index afb58f9..581c4ba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -20,6 +20,7 @@
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -28,6 +29,7 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.Restore.Input;
+import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.mail.ReplyToChangeSender;
import com.google.gerrit.server.mail.RestoredSender;
import com.google.gerrit.server.project.ChangeControl;
@@ -41,13 +43,15 @@
import java.util.Collections;
-public class Restore implements RestModifyView<ChangeResource, Input> {
+public class Restore implements RestModifyView<ChangeResource, Input>,
+ UiAction<ChangeResource> {
private static final Logger log = LoggerFactory.getLogger(Restore.class);
private final ChangeHooks hooks;
private final RestoredSender.Factory restoredSenderFactory;
private final Provider<ReviewDb> dbProvider;
private final ChangeJson json;
+ private final ChangeIndexer indexer;
public static class Input {
@DefaultInput
@@ -58,11 +62,13 @@
Restore(ChangeHooks hooks,
RestoredSender.Factory restoredSenderFactory,
Provider<ReviewDb> dbProvider,
- ChangeJson json) {
+ ChangeJson json,
+ ChangeIndexer indexer) {
this.hooks = hooks;
this.restoredSenderFactory = restoredSenderFactory;
this.dbProvider = dbProvider;
this.json = json;
+ this.indexer = indexer;
}
@Override
@@ -86,8 +92,8 @@
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
- if (change.getStatus() == Change.Status.ABANDONED) {
- change.setStatus(Change.Status.NEW);
+ if (change.getStatus() == Status.ABANDONED) {
+ change.setStatus(Status.NEW);
ChangeUtil.updated(change);
return change;
}
@@ -98,6 +104,7 @@
throw new ResourceConflictException("change is "
+ status(db.changes().get(req.getChange().getId())));
}
+ indexer.index(change);
message = newMessage(input, caller, change);
db.changeMessages().insert(Collections.singleton(message));
new ApprovalsUtil(db).syncChangeStatus(change);
@@ -116,11 +123,20 @@
}
hooks.doChangeRestoredHook(change,
caller.getAccount(),
+ db.patchSets().get(change.currentPatchSetId()),
Strings.emptyToNull(input.message),
dbProvider.get());
return json.format(change);
}
+ @Override
+ public UiAction.Description getDescription(ChangeResource resource) {
+ return new UiAction.Description()
+ .setLabel("Restore")
+ .setVisible(resource.getChange().getStatus() == Status.ABANDONED
+ && resource.getControl().canRestore());
+ }
+
private ChangeMessage newMessage(Input input, IdentifiedUser caller,
Change change) throws OrmException {
StringBuilder msg = new StringBuilder();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index 154bd64..1d4ad71 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -20,6 +20,7 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -40,7 +41,8 @@
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
-public class Revert implements RestModifyView<ChangeResource, Input> {
+public class Revert implements RestModifyView<ChangeResource, Input>,
+ UiAction<ChangeResource> {
private final ChangeHooks hooks;
private final RevertedSender.Factory revertedSenderFactory;
private final CommitValidators.Factory commitValidatorsFactory;
@@ -49,7 +51,7 @@
private final GitRepositoryManager gitManager;
private final PersonIdent myIdent;
private final PatchSetInfoFactory patchSetInfoFactory;
- private final ChangeInserter changeInserter;
+ private final ChangeInserter.Factory changeInserterFactory;
public static class Input {
public String message;
@@ -64,7 +66,7 @@
GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
@GerritPersonIdent final PersonIdent myIdent,
- final ChangeInserter changeInserter) {
+ final ChangeInserter.Factory changeInserterFactory) {
this.hooks = hooks;
this.revertedSenderFactory = revertedSenderFactory;
this.commitValidatorsFactory = commitValidatorsFactory;
@@ -72,7 +74,7 @@
this.json = json;
this.gitManager = gitManager;
this.myIdent = myIdent;
- this.changeInserter = changeInserter;
+ this.changeInserterFactory = changeInserterFactory;
this.patchSetInfoFactory = patchSetInfoFactory;
}
@@ -97,7 +99,7 @@
commitValidators,
Strings.emptyToNull(input.message), dbProvider.get(),
revertedSenderFactory, hooks, git, patchSetInfoFactory,
- myIdent, changeInserter);
+ myIdent, changeInserterFactory);
return json.format(revertedChangeId);
} catch (InvalidChangeOperationException e) {
@@ -107,6 +109,14 @@
}
}
+ @Override
+ public UiAction.Description getDescription(ChangeResource resource) {
+ return new UiAction.Description()
+ .setLabel("Revert")
+ .setVisible(resource.getChange().getStatus() == Status.MERGED
+ && resource.getControl().getRefControl().canUpload());
+ }
+
private static String status(Change change) {
return change != null ? change.getStatus().name().toLowerCase() : "deleted";
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java
index 0898bce..022f178 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java
@@ -18,6 +18,7 @@
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.AccountPatchReview;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -28,7 +29,7 @@
static class Input {
}
- static class PutReviewed implements RestModifyView<PatchResource, Input> {
+ static class PutReviewed implements RestModifyView<FileResource, Input> {
private final Provider<ReviewDb> dbProvider;
@Inject
@@ -37,14 +38,18 @@
}
@Override
- public Object apply(PatchResource resource, Input input)
+ public Object apply(FileResource resource, Input input)
throws OrmException {
ReviewDb db = dbProvider.get();
AccountPatchReview apr = getExisting(db, resource);
if (apr == null) {
- db.accountPatchReviews().insert(
- Collections.singleton(new AccountPatchReview(resource.getPatchKey(),
- resource.getAccountId())));
+ try {
+ db.accountPatchReviews().insert(
+ Collections.singleton(new AccountPatchReview(resource.getPatchKey(),
+ resource.getAccountId())));
+ } catch (OrmDuplicateKeyException e) {
+ return Response.ok("");
+ }
return Response.created("");
} else {
return Response.ok("");
@@ -52,7 +57,7 @@
}
}
- static class DeleteReviewed implements RestModifyView<PatchResource, Input> {
+ static class DeleteReviewed implements RestModifyView<FileResource, Input> {
private final Provider<ReviewDb> dbProvider;
@Inject
@@ -61,7 +66,7 @@
}
@Override
- public Object apply(PatchResource resource, Input input)
+ public Object apply(FileResource resource, Input input)
throws OrmException {
ReviewDb db = dbProvider.get();
AccountPatchReview apr = getExisting(db, resource);
@@ -73,7 +78,7 @@
}
private static AccountPatchReview getExisting(ReviewDb db,
- PatchResource resource) throws OrmException {
+ FileResource resource) throws OrmException {
AccountPatchReview.Key key = new AccountPatchReview.Key(
resource.getPatchKey(), resource.getAccountId());
return db.accountPatchReviews().get(key);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
index cdd9e0f..f891729 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
@@ -29,14 +29,23 @@
private final ChangeResource change;
private final PatchSet ps;
+ private boolean cacheable = true;
public RevisionResource(ChangeResource change, PatchSet ps) {
this.change = change;
this.ps = ps;
}
+ public boolean isCacheable() {
+ return cacheable;
+ }
+
+ public ChangeResource getChangeResource() {
+ return change;
+ }
+
public ChangeControl getControl() {
- return change.getControl();
+ return getChangeResource().getControl();
}
public Change getChange() {
@@ -48,6 +57,15 @@
}
Account.Id getAccountId() {
- return ((IdentifiedUser) getControl().getCurrentUser()).getAccountId();
+ return getUser().getAccountId();
+ }
+
+ IdentifiedUser getUser() {
+ return (IdentifiedUser) getControl().getCurrentUser();
+ }
+
+ RevisionResource doNotCache() {
+ cacheable = false;
+ return this;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
index 68ef63c..19c6d3a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
@@ -59,7 +59,7 @@
PatchSet.Id p = change.getChange().currentPatchSetId();
PatchSet ps = p != null ? dbProvider.get().patchSets().get(p) : null;
if (ps != null && visible(change, ps)) {
- return new RevisionResource(change, ps);
+ return new RevisionResource(change, ps).doNotCache();
}
throw new ResourceNotFoundException(id);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 2b51b0a..cc88efb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -24,6 +24,7 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -36,6 +37,7 @@
import com.google.gerrit.server.change.Submit.Input;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeQueue;
+import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmException;
@@ -50,7 +52,8 @@
import java.util.Collections;
import java.util.List;
-public class Submit implements RestModifyView<RevisionResource, Input> {
+public class Submit implements RestModifyView<RevisionResource, Input>,
+ UiAction<RevisionResource> {
public static class Input {
public boolean waitForMerge;
}
@@ -72,14 +75,17 @@
private final Provider<ReviewDb> dbProvider;
private final GitRepositoryManager repoManager;
private final MergeQueue mergeQueue;
+ private final ChangeIndexer indexer;
@Inject
Submit(Provider<ReviewDb> dbProvider,
GitRepositoryManager repoManager,
- MergeQueue mergeQueue) {
+ MergeQueue mergeQueue,
+ ChangeIndexer indexer) {
this.dbProvider = dbProvider;
this.repoManager = repoManager;
this.mergeQueue = mergeQueue;
+ this.indexer = indexer;
}
@Override
@@ -136,6 +142,18 @@
}
}
+ @Override
+ public UiAction.Description getDescription(RevisionResource resource) {
+ PatchSet.Id current = resource.getChange().currentPatchSetId();
+ return new UiAction.Description()
+ .setLabel(String.format(
+ "Submit Revision %d",
+ resource.getPatchSet().getPatchSetId()))
+ .setVisible(resource.getChange().getStatus().isOpen()
+ && resource.getPatchSet().getId().equals(current)
+ && resource.getControl().canSubmit());
+ }
+
/**
* If the merge was attempted and it failed the system usually writes a
* comment as a ChangeMessage and sets status to NEW. Find the relevant
@@ -187,6 +205,7 @@
} finally {
db.rollback();
}
+ indexer.index(change);
return change;
}
@@ -292,7 +311,7 @@
});
}
- private static String status(Change change) {
+ static String status(Change change) {
return change != null ? change.getStatus().name().toLowerCase() : "deleted";
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
index e7e1f32..5a4f9e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
@@ -20,6 +20,7 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.rules.RulesCache;
import com.google.gerrit.server.change.TestSubmitRule.Filters;
@@ -30,6 +31,7 @@
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
import org.kohsuke.args4j.Option;
@@ -51,8 +53,8 @@
}
@Override
- public String apply(RevisionResource rsrc, Input input) throws OrmException,
- BadRequestException, AuthException {
+ public SubmitType apply(RevisionResource rsrc, Input input)
+ throws OrmException, BadRequestException, AuthException {
if (input == null) {
input = new Input();
}
@@ -96,7 +98,16 @@
evaluator.getSubmitRule().toString(),
type));
}
- return type.toString();
+
+ String typeName = ((SymbolTerm) type).name();
+ try {
+ return SubmitType.valueOf(typeName.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException(String.format(
+ "rule %s produced invalid result: %s",
+ evaluator.getSubmitRule().toString(),
+ type));
+ }
}
static class Get implements RestReadView<RevisionResource> {
@@ -108,8 +119,8 @@
}
@Override
- public String apply(RevisionResource resource) throws BadRequestException,
- OrmException, AuthException {
+ public SubmitType apply(RevisionResource resource)
+ throws BadRequestException, OrmException, AuthException {
return test.apply(resource, null);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
index 22eae2d..fd0596e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
@@ -15,26 +15,20 @@
package com.google.gerrit.server.changedetail;
-import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals;
-import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
-
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.ReviewResult;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.mail.CreateChangeSender;
-import com.google.gerrit.server.mail.MailUtil.MailRecipients;
+import com.google.gerrit.server.mail.PatchSetNotificationSender;
import com.google.gerrit.server.mail.ReplacePatchSetSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
@@ -45,23 +39,10 @@
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.FooterLine;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import java.io.IOException;
-import java.util.Collections;
-import java.util.List;
import java.util.concurrent.Callable;
public class PublishDraft implements Callable<ReviewResult> {
- private static final Logger log =
- LoggerFactory.getLogger(PublishDraft.class);
-
public interface Factory {
PublishDraft create(PatchSet.Id patchSetId);
}
@@ -69,12 +50,8 @@
private final ChangeControl.Factory changeControlFactory;
private final ReviewDb db;
private final ChangeHooks hooks;
- private final GitRepositoryManager repoManager;
- private final PatchSetInfoFactory patchSetInfoFactory;
- private final ApprovalsUtil approvalsUtil;
- private final AccountResolver accountResolver;
- private final CreateChangeSender.Factory createChangeSenderFactory;
- private final ReplacePatchSetSender.Factory replacePatchSetFactory;
+ private final ChangeIndexer indexer;
+ private final PatchSetNotificationSender sender;
private final PatchSet.Id patchSetId;
@@ -87,16 +64,14 @@
final AccountResolver accountResolver,
final CreateChangeSender.Factory createChangeSenderFactory,
final ReplacePatchSetSender.Factory replacePatchSetFactory,
+ final ChangeIndexer indexer,
+ final PatchSetNotificationSender sender,
@Assisted final PatchSet.Id patchSetId) {
this.changeControlFactory = changeControlFactory;
this.db = db;
this.hooks = hooks;
- this.repoManager = repoManager;
- this.patchSetInfoFactory = patchSetInfoFactory;
- this.approvalsUtil = approvalsUtil;
- this.accountResolver = accountResolver;
- this.createChangeSenderFactory = createChangeSenderFactory;
- this.replacePatchSetFactory = replacePatchSetFactory;
+ this.indexer = indexer;
+ this.sender = sender;
this.patchSetId = patchSetId;
}
@@ -146,9 +121,10 @@
});
if (!updatedPatchSet.isDraft() || updatedChange.getStatus() == Change.Status.NEW) {
+ indexer.index(updatedChange);
hooks.doDraftPublishedHook(updatedChange, updatedPatchSet, db);
- sendNotifications(control.getChange().getStatus() == Change.Status.DRAFT,
+ sender.send(control.getChange().getStatus() == Change.Status.DRAFT,
(IdentifiedUser) control.getCurrentUser(), updatedChange, updatedPatchSet,
labelTypes);
}
@@ -156,66 +132,4 @@
return result;
}
-
- private void sendNotifications(final boolean newChange,
- final IdentifiedUser currentUser, final Change updatedChange,
- final PatchSet updatedPatchSet, final LabelTypes labelTypes)
- throws OrmException, IOException, PatchSetInfoNotAvailableException {
- final Repository git = repoManager.openRepository(updatedChange.getProject());
- try {
- final RevWalk revWalk = new RevWalk(git);
- final RevCommit commit;
- try {
- commit = revWalk.parseCommit(ObjectId.fromString(updatedPatchSet.getRevision().get()));
- } finally {
- revWalk.release();
- }
- final PatchSetInfo info = patchSetInfoFactory.get(commit, updatedPatchSet.getId());
- final List<FooterLine> footerLines = commit.getFooterLines();
- final Account.Id me = currentUser.getAccountId();
- final MailRecipients recipients =
- getRecipientsFromFooters(accountResolver, updatedPatchSet, footerLines);
- recipients.remove(me);
-
- if (newChange) {
- approvalsUtil.addReviewers(db, labelTypes, updatedChange, updatedPatchSet, info,
- recipients.getReviewers(), Collections.<Account.Id> emptySet());
- try {
- CreateChangeSender cm = createChangeSenderFactory.create(updatedChange);
- cm.setFrom(me);
- cm.setPatchSet(updatedPatchSet, info);
- cm.addReviewers(recipients.getReviewers());
- cm.addExtraCC(recipients.getCcOnly());
- cm.send();
- } catch (Exception e) {
- log.error("Cannot send email for new change " + updatedChange.getId(), e);
- }
- } else {
- final List<PatchSetApproval> patchSetApprovals =
- db.patchSetApprovals().byChange(updatedChange.getId()).toList();
- final MailRecipients oldRecipients =
- getRecipientsFromApprovals(patchSetApprovals);
- approvalsUtil.addReviewers(db, labelTypes, updatedChange, updatedPatchSet, info,
- recipients.getReviewers(), oldRecipients.getAll());
- final ChangeMessage msg =
- new ChangeMessage(new ChangeMessage.Key(updatedChange.getId(),
- ChangeUtil.messageUUID(db)), me,
- updatedPatchSet.getCreatedOn(), updatedPatchSet.getId());
- msg.setMessage("Uploaded patch set " + updatedPatchSet.getPatchSetId() + ".");
- try {
- ReplacePatchSetSender cm = replacePatchSetFactory.create(updatedChange);
- cm.setFrom(me);
- cm.setPatchSet(updatedPatchSet, info);
- cm.setChangeMessage(msg);
- cm.addReviewers(recipients.getReviewers());
- cm.addExtraCC(recipients.getCcOnly());
- cm.send();
- } catch (Exception e) {
- log.error("Cannot send email for new patch set " + updatedPatchSet.getId(), e);
- }
- }
- } finally {
- git.close();
- }
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
index d7bf5a3..dceecbe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -14,34 +14,27 @@
package com.google.gerrit.server.changedetail;
-import com.google.common.collect.Sets;
-import com.google.gerrit.common.ChangeHookRunner;
+import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
+
import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetAncestor;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.mail.RebasedPatchSetSender;
-import com.google.gerrit.server.mail.ReplacePatchSetSender;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -51,50 +44,35 @@
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.ThreeWayMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.Collections;
import java.util.List;
-import java.util.Set;
public class RebaseChange {
- private final ChangeControl.Factory changeControlFactory;
- private final PatchSetInfoFactory patchSetInfoFactory;
+ private final ChangeControl.GenericFactory changeControlFactory;
private final ReviewDb db;
private final GitRepositoryManager gitManager;
private final PersonIdent myIdent;
- private final GitReferenceUpdated gitRefUpdated;
- private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory;
- private final ChangeHookRunner hooks;
private final MergeUtil.Factory mergeUtilFactory;
- private final ProjectCache projectCache;
+ private final PatchSetInserter.Factory patchSetInserterFactory;
@Inject
- RebaseChange(final ChangeControl.Factory changeControlFactory,
- final PatchSetInfoFactory patchSetInfoFactory, final ReviewDb db,
+ RebaseChange(final ChangeControl.GenericFactory changeControlFactory,
+ final ReviewDb db,
@GerritPersonIdent final PersonIdent myIdent,
final GitRepositoryManager gitManager,
- final GitReferenceUpdated gitRefUpdated,
- final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
- final ChangeHookRunner hooks,
final MergeUtil.Factory mergeUtilFactory,
- final ProjectCache projectCache) {
+ final PatchSetInserter.Factory patchSetInserterFactory) {
this.changeControlFactory = changeControlFactory;
- this.patchSetInfoFactory = patchSetInfoFactory;
this.db = db;
this.gitManager = gitManager;
this.myIdent = myIdent;
- this.gitRefUpdated = gitRefUpdated;
- this.rebasedPatchSetSenderFactory = rebasedPatchSetSenderFactory;
- this.hooks = hooks;
this.mergeUtilFactory = mergeUtilFactory;
- this.projectCache = projectCache;
+ this.patchSetInserterFactory = patchSetInserterFactory;
}
/**
@@ -123,12 +101,12 @@
* @throws IOException thrown if rebase is not possible or not needed
* @throws InvalidChangeOperationException thrown if rebase is not allowed
*/
- public void rebase(final PatchSet.Id patchSetId, final Account.Id uploader)
+ public void rebase(final PatchSet.Id patchSetId, final IdentifiedUser uploader)
throws NoSuchChangeException, EmailException, OrmException, IOException,
InvalidChangeOperationException {
final Change.Id changeId = patchSetId.getParentKey();
final ChangeControl changeControl =
- changeControlFactory.validateFor(changeId);
+ changeControlFactory.validateFor(changeId, uploader);
if (!changeControl.canRebase()) {
throw new InvalidChangeOperationException(
"Cannot rebase: New patch sets are not allowed to be added to change: "
@@ -143,37 +121,19 @@
rw = new RevWalk(git);
inserter = git.newObjectInserter();
- final List<PatchSetApproval> oldPatchSetApprovals =
- db.patchSetApprovals().byChange(change.getId()).toList();
-
final String baseRev = findBaseRevision(patchSetId, db,
change.getDest(), git, null, null, null);
final RevCommit baseCommit =
rw.parseCommit(ObjectId.fromString(baseRev));
- final PatchSet newPatchSet =
- rebase(git, rw, inserter, patchSetId, change, uploader, baseCommit,
- mergeUtilFactory.create(
- changeControl.getProjectControl().getProjectState(), true));
+ PersonIdent committerIdent =
+ uploader.newCommitterIdent(myIdent.getWhen(),
+ myIdent.getTimeZone());
- final Set<Account.Id> oldReviewers = Sets.newHashSet();
- final Set<Account.Id> oldCC = Sets.newHashSet();
- for (PatchSetApproval a : oldPatchSetApprovals) {
- if (a.getValue() != 0) {
- oldReviewers.add(a.getAccountId());
- } else {
- oldCC.add(a.getAccountId());
- }
- }
- final ReplacePatchSetSender cm =
- rebasedPatchSetSenderFactory.create(change);
- cm.setFrom(uploader);
- cm.setPatchSet(newPatchSet);
- cm.addReviewers(oldReviewers);
- cm.addExtraCC(oldCC);
- cm.send();
-
- hooks.doPatchsetCreatedHook(change, newPatchSet, db);
+ rebase(git, rw, inserter, patchSetId, change,
+ uploader, baseCommit, mergeUtilFactory.create(
+ changeControl.getProjectControl().getProjectState(), true),
+ committerIdent, true, true, ValidatePolicy.GERRIT);
} catch (PathConflictException e) {
throw new IOException(e.getMessage());
} finally {
@@ -235,8 +195,7 @@
depPatchSetList = db.patchSets().byRevision(ancestorRev).toList();
}
- if (!depPatchSetList.isEmpty()) {
- PatchSet depPatchSet = depPatchSetList.get(0);
+ for (PatchSet depPatchSet : depPatchSetList) {
Change.Id depChangeId = depPatchSet.getId().getParentKey();
Change depChange;
@@ -246,6 +205,9 @@
} else {
depChange = depChangeList.get(0);
}
+ if (!depChange.getDest().equals(destBranch)) {
+ continue;
+ }
if (depChange.getStatus() == Status.ABANDONED) {
throw new IOException("Cannot rebase a change with an abandoned parent: "
@@ -261,6 +223,7 @@
db.patchSets().get(depChange.currentPatchSetId());
baseRev = latestDepPatchSet.getRevision().get();
}
+ break;
}
if (baseRev == null) {
@@ -285,17 +248,21 @@
*
* The rebased commit is added as new patch set to the change.
*
- * E-mail notification and triggering of hooks is NOT done for the creation of
- * the new patch set.
+ * E-mail notification and triggering of hooks is only done for the creation of
+ * the new patch set if `sendEmail` and `runHooks` are set to true.
*
* @param git the repository
* @param revWalk the RevWalk
* @param inserter the object inserter
* @param patchSetId the id of the patch set
- * @param chg the change that should be rebased
+ * @param change the change that should be rebased
* @param uploader the user that creates the rebased patch set
* @param baseCommit the commit that should be the new base
* @param mergeUtil merge utilities for the destination project
+ * @param committerIdent the committer's identity
+ * @param sendMail if a mail notification should be sent for the new patch set
+ * @param runHooks if hooks should be run for the new patch set
+ * @param validate if commit validation should be run for the new patch set
* @return the new patch set which is based on the given base commit
* @throws NoSuchChangeException thrown if the change to which the patch set
* belongs does not exist or is not visible to the user
@@ -305,12 +272,13 @@
*/
public PatchSet rebase(final Repository git, final RevWalk revWalk,
final ObjectInserter inserter, final PatchSet.Id patchSetId,
- final Change chg, final Account.Id uploader, final RevCommit baseCommit,
- final MergeUtil mergeUtil) throws NoSuchChangeException,
+ final Change change, final IdentifiedUser uploader, final RevCommit baseCommit,
+ final MergeUtil mergeUtil, PersonIdent committerIdent,
+ boolean sendMail, boolean runHooks, ValidatePolicy validate)
+ throws NoSuchChangeException,
OrmException, IOException, InvalidChangeOperationException,
PathConflictException {
- Change change = chg;
- if (!chg.currentPatchSetId().equals(patchSetId)) {
+ if (!change.currentPatchSetId().equals(patchSetId)) {
throw new InvalidChangeOperationException("patch set is not current");
}
final PatchSet originalPatchSet = db.patchSets().get(patchSetId);
@@ -318,84 +286,34 @@
final RevCommit rebasedCommit;
ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get());
ObjectId newId = rebaseCommit(git, inserter, revWalk.parseCommit(oldId),
- baseCommit, mergeUtil, myIdent);
+ baseCommit, mergeUtil, committerIdent);
rebasedCommit = revWalk.parseCommit(newId);
- PatchSet.Id id = ChangeUtil.nextPatchSetId(git, change.currentPatchSetId());
- final PatchSet newPatchSet = new PatchSet(id);
- newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
- newPatchSet.setUploader(uploader);
- newPatchSet.setRevision(new RevId(rebasedCommit.name()));
- newPatchSet.setDraft(originalPatchSet.isDraft());
+ final ChangeControl changeControl =
+ changeControlFactory.validateFor(change.getId(), uploader);
- final PatchSetInfo info =
- patchSetInfoFactory.get(rebasedCommit, newPatchSet.getId());
+ PatchSetInserter patchSetInserter = patchSetInserterFactory
+ .create(git, revWalk, changeControl.getRefControl(), uploader, change, rebasedCommit)
+ .setCopyLabels(true)
+ .setValidatePolicy(validate)
+ .setDraft(originalPatchSet.isDraft())
+ .setSendMail(sendMail)
+ .setRunHooks(runHooks);
- final RefUpdate ru = git.updateRef(newPatchSet.getRefName());
- ru.setExpectedOldObjectId(ObjectId.zeroId());
- ru.setNewObjectId(rebasedCommit);
- ru.disableRefLog();
- if (ru.update(revWalk) != RefUpdate.Result.NEW) {
- throw new IOException(String.format("Failed to create ref %s in %s: %s",
- newPatchSet.getRefName(), change.getDest().getParentKey().get(),
- ru.getResult()));
- }
- gitRefUpdated.fire(change.getProject(), ru);
+ final PatchSet.Id newPatchSetId = patchSetInserter.getPatchSetId();
+ final ChangeMessage cmsg =
+ new ChangeMessage(new ChangeMessage.Key(change.getId(),
+ ChangeUtil.messageUUID(db)), uploader.getAccountId(), patchSetId);
- db.changes().beginTransaction(change.getId());
- try {
- Change updatedChange = db.changes().get(change.getId());
- if (updatedChange != null && change.getStatus().isOpen()) {
- change = updatedChange;
- } else {
- throw new InvalidChangeOperationException(String.format(
- "Change %s is closed", change.getId()));
- }
+ cmsg.setMessage("Patch Set " + newPatchSetId.get()
+ + ": Patch Set " + patchSetId.get() + " was rebased");
- ChangeUtil.insertAncestors(db, newPatchSet.getId(), rebasedCommit);
- db.patchSets().insert(Collections.singleton(newPatchSet));
- updatedChange =
- db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- if (change.getStatus().isClosed()) {
- return null;
- }
- if (!change.currentPatchSetId().equals(patchSetId)) {
- return null;
- }
- if (change.getStatus() != Change.Status.DRAFT) {
- change.setStatus(Change.Status.NEW);
- }
- change.setLastSha1MergeTested(null);
- change.setCurrentPatchSet(info);
- ChangeUtil.updated(change);
- return change;
- }
- });
- if (updatedChange != null) {
- change = updatedChange;
- } else {
- throw new InvalidChangeOperationException(String.format(
- "Change %s was modified", change.getId()));
- }
+ Change newChange = patchSetInserter
+ .setMessage(cmsg)
+ .insert();
- ApprovalsUtil.copyLabels(db, projectCache.get(change.getProject())
- .getLabelTypes(), patchSetId, change.currentPatchSetId());
-
- final ChangeMessage cmsg =
- new ChangeMessage(new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(db)), uploader, patchSetId);
- cmsg.setMessage("Patch Set " + change.currentPatchSetId().get()
- + ": Patch Set " + patchSetId.get() + " was rebased");
- db.changeMessages().insert(Collections.singleton(cmsg));
- db.commit();
- } finally {
- db.rollback();
- }
-
- return newPatchSet;
+ return db.patchSets().get(newChange.currentPatchSetId());
}
/**
@@ -443,6 +361,34 @@
return objectId;
}
+ public boolean canRebase(RevisionResource r) {
+ Repository git;
+ try {
+ git = gitManager.openRepository(r.getChange().getProject());
+ } catch (RepositoryNotFoundException err) {
+ return false;
+ } catch (IOException err) {
+ return false;
+ }
+ try {
+ findBaseRevision(
+ r.getPatchSet().getId(),
+ db,
+ r.getChange().getDest(),
+ git,
+ null,
+ null,
+ null);
+ return true;
+ } catch (IOException e) {
+ return false;
+ } catch (OrmException e) {
+ return false;
+ } finally {
+ git.close();
+ }
+ }
+
public static boolean canDoRebase(final ReviewDb db,
final Change change, final GitRepositoryManager gitManager,
List<PatchSetAncestor> patchSetAncestors,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index 9a804c1..0b414f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -36,9 +36,14 @@
public class AuthConfig {
private final AuthType authType;
private final String httpHeader;
+ private final String httpDisplaynameHeader;
+ private final String httpEmailHeader;
+ private final String registerPageUrl;
private final boolean trustContainerAuth;
+ private final boolean enableRunAs;
private final boolean userNameToLowerCase;
private final boolean gitBasicAuth;
+ private final String loginUrl;
private final String logoutUrl;
private final String openIdSsoUrl;
private final List<String> openIdDomains;
@@ -56,7 +61,11 @@
throws XsrfException {
authType = toType(cfg);
httpHeader = cfg.getString("auth", null, "httpheader");
+ httpDisplaynameHeader = cfg.getString("auth", null, "httpdisplaynameheader");
+ httpEmailHeader = cfg.getString("auth", null, "httpemailheader");
+ loginUrl = cfg.getString("auth", null, "loginurl");
logoutUrl = cfg.getString("auth", null, "logouturl");
+ registerPageUrl = cfg.getString("auth", null, "registerPageUrl");
openIdSsoUrl = cfg.getString("auth", null, "openidssourl");
openIdDomains = Arrays.asList(cfg.getStringList("auth", null, "openIdDomain"));
trustedOpenIDs = toPatterns(cfg, "trustedOpenID");
@@ -64,6 +73,7 @@
cookiePath = cfg.getString("auth", null, "cookiepath");
cookieSecure = cfg.getBoolean("auth", "cookiesecure", false);
trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
+ enableRunAs = cfg.getBoolean("auth", null, "enableRunAs", true);
gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false);
userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
@@ -122,6 +132,18 @@
return httpHeader;
}
+ public String getHttpDisplaynameHeader() {
+ return httpDisplaynameHeader;
+ }
+
+ public String getHttpEmailHeader() {
+ return httpEmailHeader;
+ }
+
+ public String getLoginUrl() {
+ return loginUrl;
+ }
+
public String getLogoutURL() {
return logoutUrl;
}
@@ -164,6 +186,11 @@
return trustContainerAuth;
}
+ /** @return true if users with Run As capability can impersonate others. */
+ public boolean isRunAsEnabled() {
+ return enableRunAs;
+ }
+
/** Whether user name should be converted to lower-case before validation */
public boolean isUserNameToLowerCase() {
return userNameToLowerCase;
@@ -246,4 +273,8 @@
}
return false;
}
+
+ public String getRegisterPageUrl() {
+ return registerPageUrl;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
new file mode 100644
index 0000000..f9e9949
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.account.DefaultRealm;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.auth.AuthBackend;
+import com.google.gerrit.server.auth.InternalAuthBackend;
+import com.google.gerrit.server.auth.ldap.LdapModule;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+
+public class AuthModule extends AbstractModule {
+ private final AuthType loginType;
+
+ @Inject
+ AuthModule(AuthConfig authConfig) {
+ loginType = authConfig.getAuthType();
+ }
+
+ @Override
+ protected void configure() {
+ switch (loginType) {
+ case HTTP_LDAP:
+ case LDAP:
+ case LDAP_BIND:
+ case CLIENT_SSL_CERT_LDAP:
+ install(new LdapModule());
+ break;
+
+ case CUSTOM_EXTENSION:
+ break;
+
+ default:
+ bind(Realm.class).to(DefaultRealm.class);
+ DynamicSet.bind(binder(), AuthBackend.class).to(InternalAuthBackend.class);
+ break;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilitiesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilitiesCollection.java
new file mode 100644
index 0000000..3a8bcc5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilitiesCollection.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class CapabilitiesCollection implements
+ ChildCollection<ConfigResource, CapabilityResource> {
+ private final DynamicMap<RestView<CapabilityResource>> views;
+ private final Provider<ListCapabilities> list;
+
+ @Inject
+ CapabilitiesCollection(DynamicMap<RestView<CapabilityResource>> views,
+ Provider<ListCapabilities> list) {
+ this.views = views;
+ this.list = list;
+ }
+
+ @Override
+ public RestView<ConfigResource> list() throws ResourceNotFoundException {
+ return list.get();
+ }
+
+ @Override
+ public CapabilityResource parse(ConfigResource parent, IdString id)
+ throws ResourceNotFoundException {
+ throw new ResourceNotFoundException(id);
+ }
+
+ @Override
+ public DynamicMap<RestView<CapabilityResource>> views() {
+ return views;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
new file mode 100644
index 0000000..2e54f3e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityConstants.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import org.eclipse.jgit.nls.NLS;
+import org.eclipse.jgit.nls.TranslationBundle;
+
+public class CapabilityConstants extends TranslationBundle {
+ public static CapabilityConstants get() {
+ return NLS.getBundleFor(CapabilityConstants.class);
+ }
+
+ public String accessDatabase;
+ public String administrateServer;
+ public String createAccount;
+ public String createGroup;
+ public String createProject;
+ public String emailReviewers;
+ public String flushCaches;
+ public String killTask;
+ public String priority;
+ public String queryLimit;
+ public String runAs;
+ public String runGC;
+ public String streamEvents;
+ public String viewCaches;
+ public String viewConnections;
+ public String viewQueue;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityResource.java
new file mode 100644
index 0000000..7e3c87e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CapabilityResource.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class CapabilityResource extends ConfigResource {
+ public static final TypeLiteral<RestView<CapabilityResource>> CAPABILITY_KIND =
+ new TypeLiteral<RestView<CapabilityResource>>() {};
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java
new file mode 100644
index 0000000..5ed007a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.inject.Inject;
+
+public class ConfigCollection implements
+ RestCollection<TopLevelResource, ConfigResource> {
+ private final DynamicMap<RestView<ConfigResource>> views;
+
+ @Inject
+ ConfigCollection(DynamicMap<RestView<ConfigResource>> views) {
+ this.views = views;
+ }
+
+ @Override
+ public RestView<TopLevelResource> list() throws ResourceNotFoundException {
+ throw new ResourceNotFoundException();
+ }
+
+ @Override
+ public DynamicMap<RestView<ConfigResource>> views() {
+ return views;
+ }
+
+ @Override
+ public ConfigResource parse(TopLevelResource root, IdString id)
+ throws ResourceNotFoundException {
+ if (id.equals("server")) {
+ return new ConfigResource();
+ }
+ throw new ResourceNotFoundException(id);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigResource.java
new file mode 100644
index 0000000..ec0e0c2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigResource.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class ConfigResource implements RestResource {
+ public static final TypeLiteral<RestView<ConfigResource>> CONFIG_KIND =
+ new TypeLiteral<RestView<ConfigResource>>() {};
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 62c6863..50b8075 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -19,16 +19,19 @@
import com.google.common.cache.Cache;
import com.google.gerrit.audit.AuditModule;
import com.google.gerrit.common.ChangeListener;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.rules.PrologModule;
import com.google.gerrit.rules.RulesCache;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.CmdLineParserModule;
import com.google.gerrit.server.FileTypeRegistry;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.InternalUser;
@@ -44,7 +47,6 @@
import com.google.gerrit.server.account.AccountVisibilityProvider;
import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.ChangeUserName;
-import com.google.gerrit.server.account.DefaultRealm;
import com.google.gerrit.server.account.EmailExpander;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupCacheImpl;
@@ -57,15 +59,11 @@
import com.google.gerrit.server.account.InternalGroupBackend;
import com.google.gerrit.server.account.PerformCreateGroup;
import com.google.gerrit.server.account.PerformRenameGroup;
-import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.account.UniversalGroupBackend;
import com.google.gerrit.server.auth.AuthBackend;
-import com.google.gerrit.server.auth.InternalAuthBackend;
import com.google.gerrit.server.auth.UniversalAuthBackend;
-import com.google.gerrit.server.auth.ldap.LdapModule;
import com.google.gerrit.server.avatar.AvatarProvider;
import com.google.gerrit.server.cache.CacheRemovalListener;
-import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.events.EventFactory;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.ChangeCache;
@@ -80,6 +78,9 @@
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.git.validators.MergeValidationListener;
+import com.google.gerrit.server.git.validators.MergeValidators;
+import com.google.gerrit.server.git.validators.MergeValidators.ProjectConfigValidator;
import com.google.gerrit.server.mail.AddReviewerSender;
import com.google.gerrit.server.mail.CommitMessageEditedSender;
import com.google.gerrit.server.mail.CreateChangeSender;
@@ -88,10 +89,11 @@
import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
import com.google.gerrit.server.mail.MergeFailSender;
import com.google.gerrit.server.mail.MergedSender;
-import com.google.gerrit.server.mail.RebasedPatchSetSender;
+import com.google.gerrit.server.mail.RegisterNewEmailSender;
import com.google.gerrit.server.mail.ReplacePatchSetSender;
import com.google.gerrit.server.mail.VelocityRuntimeProvider;
import com.google.gerrit.server.patch.PatchListCacheImpl;
+import com.google.gerrit.server.patch.PatchScriptFactory;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.AccessControlModule;
import com.google.gerrit.server.project.ChangeControl;
@@ -105,7 +107,6 @@
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SectionSortCache;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
import com.google.gerrit.server.ssh.SshAddressesModule;
import com.google.gerrit.server.tools.ToolsCatalog;
import com.google.gerrit.server.util.IdGenerator;
@@ -114,45 +115,27 @@
import com.google.inject.TypeLiteral;
import org.apache.velocity.runtime.RuntimeInstance;
-import org.eclipse.jgit.lib.Config;
import java.util.List;
/** Starts global state with standard dependencies. */
public class GerritGlobalModule extends FactoryModule {
- private final AuthType loginType;
+ private final AuthModule authModule;
@Inject
- GerritGlobalModule(final AuthConfig authConfig,
- @GerritServerConfig final Config config) {
- loginType = authConfig.getAuthType();
+ GerritGlobalModule(AuthModule authModule) {
+ this.authModule = authModule;
}
@Override
protected void configure() {
- switch (loginType) {
- case HTTP_LDAP:
- case LDAP:
- case LDAP_BIND:
- case CLIENT_SSL_CERT_LDAP:
- install(new LdapModule());
- break;
-
- case CUSTOM_EXTENSION:
- break;
-
- default:
- bind(Realm.class).to(DefaultRealm.class);
- DynamicSet.bind(binder(), AuthBackend.class).to(InternalAuthBackend.class);
- break;
- }
-
bind(EmailExpander.class).toProvider(EmailExpanderProvider.class).in(
SINGLETON);
bind(IdGenerator.class);
bind(RulesCache.class);
+ install(authModule);
install(AccountByEmailCacheImpl.module());
install(AccountCacheImpl.module());
install(GroupCacheImpl.module());
@@ -164,6 +147,7 @@
install(ChangeCache.module());
install(new AccessControlModule());
+ install(new CmdLineParserModule());
install(new EmailModule());
install(new GitModule());
install(new PrologModule());
@@ -171,7 +155,6 @@
install(ThreadLocalRequestContext.module());
bind(AccountResolver.class);
- bind(ChangeQueryRewriter.class);
factory(AccountInfoCacheFactory.Factory.class);
factory(AddReviewerSender.Factory.class);
@@ -186,12 +169,13 @@
factory(MergedSender.Factory.class);
factory(MergeFailSender.Factory.class);
factory(MergeUtil.Factory.class);
+ factory(PatchScriptFactory.Factory.class);
factory(PerformCreateGroup.Factory.class);
factory(PerformRenameGroup.Factory.class);
factory(PluginUser.Factory.class);
factory(ProjectNode.Factory.class);
factory(ProjectState.Factory.class);
- factory(RebasedPatchSetSender.Factory.class);
+ factory(RegisterNewEmailSender.Factory.class);
factory(ReplacePatchSetSender.Factory.class);
factory(PerformCreateProject.Factory.class);
factory(GarbageCollection.Factory.class);
@@ -218,7 +202,6 @@
bind(TransferConfig.class);
bind(ApprovalsUtil.class);
- bind(ChangeInserter.class);
bind(ChangeMergeQueue.class).in(SINGLETON);
bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
factory(ReloadSubmitQueueOp.Factory.class);
@@ -236,24 +219,32 @@
bind(AccountControl.Factory.class);
install(new AuditModule());
+ install(new com.google.gerrit.server.access.Module());
install(new com.google.gerrit.server.account.Module());
install(new com.google.gerrit.server.change.Module());
+ install(new com.google.gerrit.server.config.Module());
install(new com.google.gerrit.server.group.Module());
install(new com.google.gerrit.server.project.Module());
bind(GitReferenceUpdated.class);
DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
DynamicSet.setOf(binder(), CacheRemovalListener.class);
+ DynamicMap.mapOf(binder(), CapabilityDefinition.class);
DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
+ DynamicSet.setOf(binder(), ProjectDeletedListener.class);
DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ChangeCache.class);
DynamicSet.setOf(binder(), ChangeListener.class);
DynamicSet.setOf(binder(), CommitValidationListener.class);
+ DynamicSet.setOf(binder(), MergeValidationListener.class);
DynamicItem.itemOf(binder(), AvatarProvider.class);
+ DynamicSet.setOf(binder(), LifecycleListener.class);
bind(AnonymousUser.class);
factory(CommitValidators.Factory.class);
+ factory(MergeValidators.Factory.class);
+ factory(ProjectConfigValidator.Factory.class);
factory(NotesBranchUtil.Factory.class);
bind(AccountManager.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetVersion.java
new file mode 100644
index 0000000..f618959c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetVersion.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.common.Version;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+
+public class GetVersion implements RestReadView<ConfigResource> {
+ @Override
+ public String apply(ConfigResource resource) throws ResourceNotFoundException {
+ String version = Version.getVersion();
+ if (version == null) {
+ throw new ResourceNotFoundException();
+ }
+ return version;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
index 8b517a3..641a48f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
@@ -16,6 +16,8 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.util.ServerRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Config;
@@ -25,8 +27,10 @@
public class GitReceivePackGroupsProvider extends GroupSetProvider {
@Inject
public GitReceivePackGroupsProvider(GroupBackend gb,
- @GerritServerConfig Config config) {
- super(gb, config, "receive", null, "allowGroup");
+ @GerritServerConfig Config config,
+ ThreadLocalRequestContext threadContext,
+ ServerRequestContext serverCtx) {
+ super(gb, config, threadContext, serverCtx, "receive", null, "allowGroup");
// If no group was set, default to "registered users"
//
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
index c519902..edae46b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
@@ -16,6 +16,8 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.util.ServerRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Config;
@@ -26,8 +28,10 @@
public class GitUploadPackGroupsProvider extends GroupSetProvider {
@Inject
public GitUploadPackGroupsProvider(GroupBackend gb,
- @GerritServerConfig Config config) {
- super(gb, config, "upload", null, "allowGroup");
+ @GerritServerConfig Config config,
+ ThreadLocalRequestContext threadContext,
+ ServerRequestContext serverCtx) {
+ super(gb, config, threadContext, serverCtx, "upload", null, "allowGroup");
// If no group was set, default to "registered users" and "anonymous"
//
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
index 5fa243b..5c3ec39 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -19,6 +19,9 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupBackends;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ServerRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -37,19 +40,26 @@
@Inject
protected GroupSetProvider(GroupBackend groupBackend,
- @GerritServerConfig Config config, String section,
+ @GerritServerConfig Config config,
+ ThreadLocalRequestContext threadContext,
+ ServerRequestContext serverCtx, String section,
String subsection, String name) {
- String[] groupNames = config.getStringList(section, subsection, name);
- ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
- for (String n : groupNames) {
- GroupReference g = GroupBackends.findBestSuggestion(groupBackend, n);
- if (g == null) {
- log.warn("Group \"{0}\" not in database, skipping.", n);
- } else {
- builder.add(g.getUUID());
+ RequestContext ctx = threadContext.setContext(serverCtx);
+ try {
+ String[] groupNames = config.getStringList(section, subsection, name);
+ ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
+ for (String n : groupNames) {
+ GroupReference g = GroupBackends.findBestSuggestion(groupBackend, n);
+ if (g == null) {
+ log.warn("Group \"{}\" not in database, skipping.", n);
+ } else {
+ builder.add(g.getUUID());
+ }
}
+ groupIds = builder.build();
+ } finally {
+ threadContext.setContext(ctx);
}
- groupIds = builder.build();
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java
new file mode 100644
index 0000000..c64f786
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCapabilities.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+
+/** List capabilities visible to the calling user. */
+public class ListCapabilities implements RestReadView<ConfigResource> {
+ private static final Logger log = LoggerFactory.getLogger(ListCapabilities.class);
+ private final DynamicMap<CapabilityDefinition> pluginCapabilities;
+
+ @Inject
+ public ListCapabilities(DynamicMap<CapabilityDefinition> pluginCapabilities) {
+ this.pluginCapabilities = pluginCapabilities;
+ }
+
+ @Override
+ public Map<String, CapabilityInfo> apply(ConfigResource resource)
+ throws AuthException, BadRequestException, ResourceConflictException,
+ IllegalArgumentException, SecurityException, IllegalAccessException,
+ NoSuchFieldException {
+ Map<String, CapabilityInfo> output = Maps.newTreeMap();
+ collectCoreCapabilities(output);
+ collectPluginCapabilities(output);
+ return output;
+ }
+
+ private void collectCoreCapabilities(Map<String, CapabilityInfo> output)
+ throws IllegalAccessException, NoSuchFieldException {
+ Class<? extends CapabilityConstants> bundleClass =
+ CapabilityConstants.get().getClass();
+ CapabilityConstants c = CapabilityConstants.get();
+ for (String id : GlobalCapability.getAllNames()) {
+ String name = (String) bundleClass.getField(id).get(c);
+ output.put(id, new CapabilityInfo(id, name));
+ }
+ }
+
+ private void collectPluginCapabilities(Map<String, CapabilityInfo> output) {
+ for (String pluginName : pluginCapabilities.plugins()) {
+ if (!isPluginNameSane(pluginName)) {
+ log.warn(String.format(
+ "Plugin name %s must match [A-Za-z0-9-]+ to use capabilities;"
+ + " rename the plugin",
+ pluginName));
+ continue;
+ }
+ for (Map.Entry<String, Provider<CapabilityDefinition>> entry :
+ pluginCapabilities.byPlugin(pluginName).entrySet()) {
+ String id = String.format("%s-%s", pluginName, entry.getKey());
+ output.put(id, new CapabilityInfo(
+ id,
+ entry.getValue().get().getDescription()));
+ }
+ }
+ }
+
+ private static boolean isPluginNameSane(String pluginName) {
+ return CharMatcher.JAVA_LETTER_OR_DIGIT
+ .or(CharMatcher.is('-'))
+ .matchesAllOf(pluginName);
+ }
+
+ public static class CapabilityInfo {
+ final String kind = "gerritcodereview#capability";
+ public String id;
+ public String name;
+
+ public CapabilityInfo(String id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
new file mode 100644
index 0000000..0ea7390
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
+import static com.google.gerrit.server.config.CapabilityResource.CAPABILITY_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+
+public class Module extends RestApiModule {
+ @Override
+ protected void configure() {
+ DynamicMap.mapOf(binder(), CONFIG_KIND);
+ DynamicMap.mapOf(binder(), CAPABILITY_KIND);
+ child(CONFIG_KIND, "capabilities").to(CapabilitiesCollection.class);
+ get(CONFIG_KIND, "version").to(GetVersion.class);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index 6622b0f..0189de3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -15,6 +15,8 @@
package com.google.gerrit.server.config;
import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.util.ServerRequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Config;
@@ -33,7 +35,9 @@
public class ProjectOwnerGroupsProvider extends GroupSetProvider {
@Inject
public ProjectOwnerGroupsProvider(GroupBackend gb,
- @GerritServerConfig final Config config) {
- super(gb, config, "repository", "*", "ownerGroup");
+ @GerritServerConfig final Config config,
+ ThreadLocalRequestContext context,
+ ServerRequestContext serverCtx) {
+ super(gb, config, context, serverCtx, "repository", "*", "ownerGroup");
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index 2116c0c..9d7c54a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -40,6 +40,7 @@
public final File hooks_dir;
public final File static_dir;
public final File themes_dir;
+ public final File index_dir;
public final File gerrit_sh;
public final File gerrit_war;
@@ -77,6 +78,7 @@
hooks_dir = new File(site_path, "hooks");
static_dir = new File(site_path, "static");
themes_dir = new File(site_path, "themes");
+ index_dir = new File(site_path, "index");
gerrit_sh = new File(bin_dir, "gerrit.sh");
gerrit_war = new File(bin_dir, "gerrit.war");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
index 79d82e3..91df974 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/PatchSetAttribute.java
@@ -24,6 +24,7 @@
public AccountAttribute uploader;
public Long createdOn;
public AccountAttribute author;
+ public boolean isDraft;
public List<ApprovalAttribute> approvals;
public List<PatchSetCommentAttribute> comments;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index 63bfa71..02d8a01 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -45,12 +45,14 @@
import com.google.gerrit.server.data.SubmitLabelAttribute;
import com.google.gerrit.server.data.SubmitRecordAttribute;
import com.google.gerrit.server.data.TrackingIdAttribute;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListEntry;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
@@ -78,19 +80,24 @@
private final SchemaFactory<ReviewDb> schema;
private final PatchSetInfoFactory psInfoFactory;
private final PersonIdent myIdent;
+ private final Provider<ReviewDb> db;
+ private final GitRepositoryManager repoManager;
@Inject
EventFactory(AccountCache accountCache,
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
PatchSetInfoFactory psif,
PatchListCache patchListCache, SchemaFactory<ReviewDb> schema,
- @GerritPersonIdent PersonIdent myIdent) {
+ @GerritPersonIdent PersonIdent myIdent,
+ Provider<ReviewDb> db, GitRepositoryManager repoManager) {
this.accountCache = accountCache;
this.urlProvider = urlProvider;
this.patchListCache = patchListCache;
this.schema = schema;
this.psInfoFactory = psif;
this.myIdent = myIdent;
+ this.db = db;
+ this.repoManager = repoManager;
}
/**
@@ -108,8 +115,15 @@
a.id = change.getKey().get();
a.number = change.getId().toString();
a.subject = change.getSubject();
+ try {
+ a.commitMessage = new ChangeData(change).commitMessage(repoManager, db);
+ } catch (Exception e) {
+ log.error("Error while getting full commit message for"
+ + " change " + a.number);
+ }
a.url = getChangeUrl(change);
a.owner = asAccountAttribute(change.getOwner());
+ a.status = change.getStatus();
return a;
}
@@ -117,7 +131,8 @@
* Create a RefUpdateAttribute for the given old ObjectId, new ObjectId, and
* branch that is suitable for serialization to JSON.
*
- * @param refUpdate
+ * @param oldId
+ * @param newId
* @param refName
* @return object suitable for serialization to JSON
*/
@@ -141,7 +156,6 @@
a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L;
a.sortKey = change.getSortKey();
a.open = change.getStatus().isOpen();
- a.status = change.getStatus();
}
/**
@@ -361,6 +375,7 @@
p.ref = patchSet.getRefName();
p.uploader = asAccountAttribute(patchSet.getUploader());
p.createdOn = patchSet.getCreatedOn().getTime() / 1000L;
+ p.isDraft = patchSet.isDraft();
final PatchSet.Id pId = patchSet.getId();
try {
final ReviewDb db = schema.open();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java
new file mode 100644
index 0000000..e725eac
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/TopicChangedEvent.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+import com.google.gerrit.server.data.AccountAttribute;
+import com.google.gerrit.server.data.ChangeAttribute;
+
+public class TopicChangedEvent extends ChangeEvent {
+ public final String type = "topic-changed";
+ public ChangeAttribute change;
+ public AccountAttribute changer;
+ public String oldTopic;
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index 49c90bd..04fccc2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -22,11 +22,16 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
public class GitReferenceUpdated {
+ private static final Logger log = LoggerFactory
+ .getLogger(GitReferenceUpdated.class);
+
public static final GitReferenceUpdated DISABLED = new GitReferenceUpdated(
Collections.<GitReferenceUpdatedListener> emptyList());
@@ -52,7 +57,11 @@
ObjectId n = newObjectId != null ? newObjectId : ObjectId.zeroId();
Event event = new Event(project, ref, o.name(), n.name());
for (GitReferenceUpdatedListener l : listeners) {
- l.onGitReferenceUpdated(event);
+ try {
+ l.onGitReferenceUpdated(event);
+ } catch (RuntimeException e) {
+ log.warn("Failure in GitReferenceUpdatedListener", e);
+ }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
new file mode 100644
index 0000000..fa64f4a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
@@ -0,0 +1,142 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.extensions.webui;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.webui.PrivateInternals_UiActionDescription;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityUtils;
+import com.google.inject.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+public class UiActions {
+ private static final Logger log = LoggerFactory.getLogger(UiActions.class);
+
+ public static Predicate<UiAction.Description> enabled() {
+ return new Predicate<UiAction.Description>() {
+ @Override
+ public boolean apply(UiAction.Description input) {
+ return input.isEnabled();
+ }
+ };
+ }
+
+ public static List<UiAction.Description> sorted(Iterable<UiAction.Description> in) {
+ List<UiAction.Description> s = Lists.newArrayList(in);
+ Collections.sort(s, new Comparator<UiAction.Description>() {
+ @Override
+ public int compare(UiAction.Description a, UiAction.Description b) {
+ return a.getId().compareTo(b.getId());
+ }
+ });
+ return s;
+ }
+
+ public static Iterable<UiAction.Description> plugins(Iterable<UiAction.Description> in) {
+ return Iterables.filter(in,
+ new Predicate<UiAction.Description>() {
+ @Override
+ public boolean apply(UiAction.Description input) {
+ return input.getId().indexOf('~') > 0;
+ }
+ });
+ }
+
+ public static <R extends RestResource> Iterable<UiAction.Description> from(
+ RestCollection<?, R> collection,
+ R resource,
+ Provider<CurrentUser> userProvider) {
+ return from(collection.views(), resource, userProvider);
+ }
+
+ public static <R extends RestResource> Iterable<UiAction.Description> from(
+ DynamicMap<RestView<R>> views,
+ final R resource,
+ final Provider<CurrentUser> userProvider) {
+ return Iterables.filter(
+ Iterables.transform(
+ views,
+ new Function<DynamicMap.Entry<RestView<R>>, UiAction.Description> () {
+ @Override
+ @Nullable
+ public UiAction.Description apply(DynamicMap.Entry<RestView<R>> e) {
+ int d = e.getExportName().indexOf('.');
+ if (d < 0) {
+ return null;
+ }
+
+ RestView<R> view;
+ try {
+ view = e.getProvider().get();
+ } catch (RuntimeException err) {
+ log.error(String.format(
+ "error creating view %s.%s",
+ e.getPluginName(), e.getExportName()), err);
+ return null;
+ }
+
+ if (!(view instanceof UiAction)) {
+ return null;
+ }
+
+ try {
+ CapabilityUtils.checkRequiresCapability(userProvider,
+ e.getPluginName(), view.getClass());
+ } catch (AuthException exc) {
+ return null;
+ }
+
+ UiAction.Description dsc =
+ ((UiAction<R>) view).getDescription(resource);
+ if (dsc == null || !dsc.isVisible()) {
+ return null;
+ }
+
+ String name = e.getExportName().substring(d + 1);
+ PrivateInternals_UiActionDescription.setMethod(
+ dsc,
+ e.getExportName().substring(0, d));
+ PrivateInternals_UiActionDescription.setId(
+ dsc,
+ "gerrit".equals(e.getPluginName())
+ ? name
+ : e.getPluginName() + '~' + name);
+ return dsc;
+ }
+ }),
+ Predicates.notNull());
+ }
+
+ private UiActions() {
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
index 6f71936..3262b23 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
@@ -10,7 +10,7 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
-// limitations under the License.package com.google.gerrit.server.git;
+// limitations under the License.
package com.google.gerrit.server.git;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
index ce5308f..f4ed7cd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
@@ -148,8 +148,9 @@
final String cherryPickCmtMsg = args.mergeUtil.createCherryPickCommitMessage(n);
final CodeReviewCommit newCommit =
- args.mergeUtil.createCherryPickFromCommit(args.repo, args.inserter, mergeTip, n,
- cherryPickCommitterIdent, cherryPickCmtMsg, args.rw);
+ (CodeReviewCommit) args.mergeUtil.createCherryPickFromCommit(args.repo,
+ args.inserter, mergeTip, n, cherryPickCommitterIdent,
+ cherryPickCmtMsg, args.rw);
if (newCommit == null) {
return null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
index 86d79e0..311856d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -24,7 +24,7 @@
import java.util.List;
/** Extended commit entity with code review specific metadata. */
-class CodeReviewCommit extends RevCommit {
+public class CodeReviewCommit extends RevCommit {
static CodeReviewCommit error(final CommitMergeStatus s) {
final CodeReviewCommit r = new CodeReviewCommit(ObjectId.zeroId());
r.statusCode = s;
@@ -59,7 +59,7 @@
/** Commits which are missing ancestors of this commit. */
List<CodeReviewCommit> missing;
- CodeReviewCommit(final AnyObjectId id) {
+ public CodeReviewCommit(final AnyObjectId id) {
super(id);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 1ca74b1..c51a6ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -164,10 +164,23 @@
// on disk; for instance when the project has been created directly on the
// file-system through replication.
//
- if (FileKey.resolve(gitDirOf(name), FS.DETECTED) != null) {
- onCreateProject(name);
+ if (!name.get().endsWith(Constants.DOT_GIT_EXT)) {
+ if (FileKey.resolve(gitDirOf(name), FS.DETECTED) != null) {
+ onCreateProject(name);
+ } else {
+ throw new RepositoryNotFoundException(gitDirOf(name));
+ }
} else {
- throw new RepositoryNotFoundException(gitDirOf(name));
+ final File directory = gitDirOf(name);
+ if (FileKey.isGitRepository(new File(directory, Constants.DOT_GIT),
+ FS.DETECTED)) {
+ onCreateProject(name);
+ } else if (FileKey.isGitRepository(new File(directory.getParentFile(),
+ directory.getName() + Constants.DOT_GIT_EXT), FS.DETECTED)) {
+ onCreateProject(name);
+ } else {
+ throw new RepositoryNotFoundException(gitDirOf(name));
+ }
}
}
final FileKey loc = FileKey.lenient(gitDirOf(name), FS.DETECTED);
@@ -357,7 +370,9 @@
for (File f : ls) {
String fileName = f.getName();
- if (FileKey.isGitRepository(f, FS.DETECTED)) {
+ if (fileName.equals(Constants.DOT_GIT)) {
+ // Skip repositories named only `.git`
+ } else if (FileKey.isGitRepository(f, FS.DETECTED)) {
Project.NameKey nameKey = getProjectName(prefix, fileName);
if (isUnreasonableName(nameKey)) {
log.warn("Ignoring unreasonably named repository " + f.getAbsolutePath());
@@ -374,10 +389,7 @@
private Project.NameKey getProjectName(final String prefix,
final String fileName) {
final String projectName;
- if (fileName.equals(Constants.DOT_GIT)) {
- projectName = prefix.substring(0, prefix.length() - 1);
-
- } else if (fileName.endsWith(Constants.DOT_GIT_EXT)) {
+ if (fileName.endsWith(Constants.DOT_GIT_EXT)) {
int newLen = fileName.length() - Constants.DOT_GIT_EXT.length();
projectName = prefix + fileName.substring(0, newLen);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 3a9eaa3..c4d06d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -15,7 +15,7 @@
package com.google.gerrit.server.git;
import static com.google.gerrit.server.git.MergeUtil.getSubmitter;
-import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -38,11 +38,14 @@
import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.validators.MergeValidationException;
+import com.google.gerrit.server.git.validators.MergeValidators;
+import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.mail.MergeFailSender;
import com.google.gerrit.server.mail.MergedSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -54,7 +57,6 @@
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.util.RequestScopePropagator;
import com.google.gwtorm.server.AtomicUpdate;
-import com.google.gwtorm.server.OrmConcurrencyException;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
@@ -85,9 +87,10 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.Set;
+import javax.annotation.Nullable;
+
/**
* Merges changes in submission order into a single branch.
* <p>
@@ -116,8 +119,8 @@
private static final long LOCK_FAILURE_RETRY_DELAY =
MILLISECONDS.convert(15, SECONDS);
- private static final long DUPLICATE_MESSAGE_INTERVAL =
- MILLISECONDS.convert(1, DAYS);
+ private static final long MAX_SUBMIT_WINDOW =
+ MILLISECONDS.convert(12, HOURS);
private final GitRepositoryManager repoManager;
private final SchemaFactory<ReviewDb> schemaFactory;
@@ -130,6 +133,7 @@
private final IdentifiedUser.GenericFactory identifiedUserFactory;
private final ChangeControl.GenericFactory changeControlFactory;
private final MergeQueue mergeQueue;
+ private final MergeValidators.Factory mergeValidatorsFactory;
private final Branch.NameKey destBranch;
private ProjectState destProject;
@@ -152,7 +156,7 @@
private final SubmoduleOp.Factory subOpFactory;
private final WorkQueue workQueue;
private final RequestScopePropagator requestScopePropagator;
- private final AllProjectsName allProjectsName;
+ private final ChangeIndexer indexer;
@Inject
MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
@@ -168,7 +172,8 @@
final SubmoduleOp.Factory subOpFactory,
final WorkQueue workQueue,
final RequestScopePropagator requestScopePropagator,
- final AllProjectsName allProjectsName) {
+ final ChangeIndexer indexer,
+ final MergeValidators.Factory mergeValidatorsFactory) {
repoManager = grm;
schemaFactory = sf;
labelNormalizer = fs;
@@ -187,68 +192,14 @@
this.subOpFactory = subOpFactory;
this.workQueue = workQueue;
this.requestScopePropagator = requestScopePropagator;
- this.allProjectsName = allProjectsName;
+ this.indexer = indexer;
+ this.mergeValidatorsFactory = mergeValidatorsFactory;
destBranch = branch;
toMerge = ArrayListMultimap.create();
potentiallyStillSubmittable = new ArrayList<CodeReviewCommit>();
commits = new HashMap<Change.Id, CodeReviewCommit>();
}
- public void verifyMergeability(Change change) throws NoSuchProjectException {
- try {
- setDestProject();
- openRepository();
- final Ref destBranchRef = repo.getRef(destBranch.get());
-
- // Test mergeability of the change if the last merged sha1
- // in the branch is different from the last sha1
- // the change was tested against.
- if ((destBranchRef == null && change.getLastSha1MergeTested() == null)
- || change.getLastSha1MergeTested() == null
- || (destBranchRef != null && !destBranchRef.getObjectId().getName()
- .equals(change.getLastSha1MergeTested().get()))) {
- openSchema();
- openBranch();
- validateChangeList(Collections.singletonList(change));
- if (!toMerge.isEmpty()) {
- final Entry<SubmitType, CodeReviewCommit> e =
- toMerge.entries().iterator().next();
- final boolean isMergeable =
- createStrategy(e.getKey()).dryRun(branchTip, e.getValue());
-
- // update sha1 tested merge.
- if (destBranchRef != null) {
- change.setLastSha1MergeTested(new RevId(destBranchRef
- .getObjectId().getName()));
- } else {
- change.setLastSha1MergeTested(new RevId(""));
- }
- change.setMergeable(isMergeable);
- db.changes().update(Collections.singleton(change));
- } else {
- log.error("Test merge attempt for change: " + change.getId()
- + " failed");
- }
- }
- } catch (MergeException e) {
- log.error("Test merge attempt for change: " + change.getId()
- + " failed", e);
- } catch (OrmException e) {
- log.error("Test merge attempt for change: " + change.getId()
- + " failed: Not able to query the database", e);
- } catch (IOException e) {
- log.error("Test merge attempt for change: " + change.getId()
- + " failed", e);
- } finally {
- if (repo != null) {
- repo.close();
- }
- if (db != null) {
- db.close();
- }
- }
- }
-
private void setDestProject() throws MergeException {
destProject = projectCache.get(destBranch.getParentKey());
if (destProject == null) {
@@ -262,15 +213,17 @@
}
}
- public void merge() throws MergeException, NoSuchProjectException {
+ public void merge() throws MergeException {
setDestProject();
try {
openSchema();
openRepository();
- openBranch();
+
+ RefUpdate branchUpdate = openBranch();
+ boolean reopen = false;
+
final ListMultimap<SubmitType, Change> toSubmit =
validateChangeList(db.changes().submitted(destBranch).toList());
-
final ListMultimap<SubmitType, CodeReviewCommit> toMergeNextTurn =
ArrayListMultimap.create();
final List<CodeReviewCommit> potentiallyStillSubmittableOnNextRun =
@@ -280,10 +233,14 @@
final Set<SubmitType> submitTypes =
new HashSet<Project.SubmitType>(toMerge.keySet());
for (final SubmitType submitType : submitTypes) {
- final RefUpdate branchUpdate = openBranch();
+ if (reopen) {
+ branchUpdate = openBranch();
+ }
final SubmitStrategy strategy = createStrategy(submitType);
preMerge(strategy, toMerge.get(submitType));
updateBranch(strategy, branchUpdate);
+ reopen = true;
+
updateChangeStatus(toSubmit.get(submitType));
updateSubscriptions(toSubmit.get(submitType));
@@ -315,6 +272,11 @@
message(commit.change, capable.getMessage()), false);
}
}
+ } catch (NoSuchProjectException noProject) {
+ log.warn(String.format(
+ "Project %s no longer exists, abandoning open changes",
+ destBranch.getParentKey().get()));
+ abandonAllOpenChanges();
} catch (OrmException e) {
throw new MergeException("Cannot query the database", e);
} finally {
@@ -388,13 +350,12 @@
canMergeFlag, getAlreadyAccepted(branchTip), destBranch);
}
- private void openRepository() throws MergeException {
+ private void openRepository() throws MergeException, NoSuchProjectException {
final Project.NameKey name = destBranch.getParentKey();
try {
repo = repoManager.openRepository(name);
- } catch (RepositoryNotFoundException notGit) {
- final String m = "Repository \"" + name.get() + "\" unknown.";
- throw new MergeException(m, notGit);
+ } catch (RepositoryNotFoundException notFound) {
+ throw new NoSuchProjectException(name, notFound);
} catch (IOException err) {
final String m = "Error opening repository \"" + name.get() + '"';
throw new MergeException(m, err);
@@ -419,27 +380,15 @@
if (branchUpdate.getOldObjectId() != null) {
branchTip =
(CodeReviewCommit) rw.parseCommit(branchUpdate.getOldObjectId());
- } else {
+ } else if (repo.getFullBranch().equals(destBranch.get())) {
branchTip = null;
- }
-
- try {
- final Ref destRef = repo.getRef(destBranch.get());
- if (destRef != null) {
- branchUpdate.setExpectedOldObjectId(destRef.getObjectId());
- } else if (repo.getFullBranch().equals(destBranch.get())) {
- branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
- } else {
- for (final Change c : db.changes().submitted(destBranch).toList()) {
- setNew(c, message(c, "Your change could not be merged, "
- + "because the destination branch does not exist anymore."));
- }
+ branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
+ } else {
+ for (final Change c : db.changes().submitted(destBranch).toList()) {
+ setNew(c, message(c, "Your change could not be merged, "
+ + "because the destination branch does not exist anymore."));
}
- } catch (IOException e) {
- throw new MergeException(
- "Failed to check existence of destination branch", e);
}
-
return branchUpdate;
} catch (IOException e) {
throw new MergeException("Cannot open branch", e);
@@ -538,50 +487,12 @@
continue;
}
- if (GitRepositoryManager.REF_CONFIG.equals(destBranch.get())) {
- final Project.NameKey newParent;
- try {
- ProjectConfig cfg =
- new ProjectConfig(destProject.getProject().getNameKey());
- cfg.load(repo, commit);
- newParent = cfg.getProject().getParent(allProjectsName);
- } catch (Exception e) {
- commits.put(changeId, CodeReviewCommit
- .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION));
- continue;
- }
- final Project.NameKey oldParent =
- destProject.getProject().getParent(allProjectsName);
- if (oldParent == null) {
- // update of the 'All-Projects' project
- if (newParent != null) {
- commits.put(changeId, CodeReviewCommit
- .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT));
- continue;
- }
- } else {
- if (!oldParent.equals(newParent)) {
- final PatchSetApproval psa = getSubmitter(db, ps.getId());
- if (psa == null) {
- commits.put(changeId, CodeReviewCommit
- .error(CommitMergeStatus.SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN));
- continue;
- }
- final IdentifiedUser submitter =
- identifiedUserFactory.create(psa.getAccountId());
- if (!submitter.getCapabilities().canAdministrateServer()) {
- commits.put(changeId, CodeReviewCommit
- .error(CommitMergeStatus.SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN));
- continue;
- }
-
- if (projectCache.get(newParent) == null) {
- commits.put(changeId, CodeReviewCommit
- .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND));
- continue;
- }
- }
- }
+ MergeValidators mergeValidators = mergeValidatorsFactory.create();
+ try {
+ mergeValidators.validatePreMerge(repo, commit, destProject, destBranch, ps.getId());
+ } catch (MergeValidationException mve) {
+ commits.put(changeId, CodeReviewCommit.error(mve.getStatus()));
+ continue;
}
commit.change = chg;
@@ -897,7 +808,7 @@
try {
hooks.doChangeMergedHook(c,
accountCache.get(submitter.getAccountId()).getAccount(),
- db.patchSets().get(c.currentPatchSetId()), db);
+ db.patchSets().get(commit.patchsetId), db);
} catch (OrmException ex) {
log.error("Cannot run hook for submitted patch set " + c.getId(), ex);
}
@@ -905,6 +816,7 @@
} finally {
db.rollback();
}
+ indexer.index(c);
}
private void setMergedPatchSet(Change.Id changeId, final PatchSet.Id merged)
@@ -1026,61 +938,41 @@
sendMergeFail(c, msg, true);
}
- private boolean isDuplicate(ChangeMessage msg) {
+ private enum RetryStatus {
+ UNSUBMIT, RETRY_NO_MESSAGE, RETRY_ADD_MESSAGE;
+ }
+
+ private RetryStatus getRetryStatus(
+ @Nullable PatchSetApproval submitter,
+ ChangeMessage msg) {
+ if (submitter != null
+ && System.currentTimeMillis() - submitter.getGranted().getTime()
+ > MAX_SUBMIT_WINDOW) {
+ return RetryStatus.UNSUBMIT;
+ }
+
try {
ChangeMessage last = Iterables.getLast(db.changeMessages().byChange(
msg.getPatchSetId().getParentKey()), null);
if (last != null) {
- long lastMs = last.getWrittenOn().getTime();
- long msgMs = msg.getWrittenOn().getTime();
if (Objects.equal(last.getAuthor(), msg.getAuthor())
- && Objects.equal(last.getMessage(), msg.getMessage())
- && msgMs - lastMs < DUPLICATE_MESSAGE_INTERVAL) {
- return true;
+ && Objects.equal(last.getMessage(), msg.getMessage())) {
+ long lastMs = last.getWrittenOn().getTime();
+ long msgMs = msg.getWrittenOn().getTime();
+ return msgMs - lastMs > MAX_SUBMIT_WINDOW
+ ? RetryStatus.UNSUBMIT
+ : RetryStatus.RETRY_NO_MESSAGE;
}
}
+ return RetryStatus.RETRY_ADD_MESSAGE;
} catch (OrmException err) {
- log.warn("Cannot check previous merge failure message", err);
+ log.warn("Cannot check previous merge failure, unsubmitting", err);
+ return RetryStatus.UNSUBMIT;
}
- return false;
}
private void sendMergeFail(final Change c, final ChangeMessage msg,
- final boolean makeNew) {
- if (isDuplicate(msg)) {
- return;
- }
-
- try {
- db.changeMessages().insert(Collections.singleton(msg));
- } catch (OrmException err) {
- log.warn("Cannot record merge failure message", err);
- }
-
- if (makeNew) {
- try {
- db.changes().atomicUpdate(c.getId(), new AtomicUpdate<Change>() {
- @Override
- public Change update(Change c) {
- if (c.getStatus().isOpen()) {
- c.setStatus(Change.Status.NEW);
- ChangeUtil.updated(c);
- }
- return c;
- }
- });
- } catch (OrmConcurrencyException err) {
- } catch (OrmException err) {
- log.warn("Cannot update change status", err);
- }
- } else {
- try {
- ChangeUtil.touch(c, db);
- } catch (OrmException err) {
- log.warn("Cannot update change timestamp", err);
- }
- }
-
+ boolean makeNew) {
PatchSetApproval submitter = null;
try {
submitter = getSubmitter(db, c.currentPatchSetId());
@@ -1088,6 +980,43 @@
log.error("Cannot get submitter", e);
}
+ if (!makeNew) {
+ RetryStatus retryStatus = getRetryStatus(submitter, msg);
+ if (retryStatus == RetryStatus.RETRY_NO_MESSAGE) {
+ return;
+ } else if (retryStatus == RetryStatus.UNSUBMIT) {
+ makeNew = true;
+ }
+ }
+
+ final boolean setStatusNew = makeNew;
+ try {
+ db.changes().beginTransaction(c.getId());
+ try {
+ Change change = db.changes().atomicUpdate(
+ c.getId(),
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change c) {
+ if (c.getStatus().isOpen()) {
+ if (setStatusNew) {
+ c.setStatus(Change.Status.NEW);
+ }
+ ChangeUtil.updated(c);
+ }
+ return c;
+ }
+ });
+ db.changeMessages().insert(Collections.singleton(msg));
+ db.commit();
+ indexer.index(change);
+ } finally {
+ db.rollback();
+ }
+ } catch (OrmException err) {
+ log.warn("Cannot record merge failure message", err);
+ }
+
final PatchSetApproval from = submitter;
workQueue.getDefaultQueue()
.submit(requestScopePropagator.wrap(new Runnable() {
@@ -1135,4 +1064,53 @@
}
}
}
+
+ private void abandonAllOpenChanges() {
+ try {
+ openSchema();
+ for (Change c : db.changes().byProjectOpenAll(destBranch.getParentKey())) {
+ abandonOneChange(c);
+ }
+ db.close();
+ db = null;
+ } catch (OrmException e) {
+ log.warn(String.format(
+ "Cannot abandon changes for deleted project %s",
+ destBranch.getParentKey().get()), e);
+ }
+ }
+
+ private void abandonOneChange(Change change) throws OrmException {
+ db.changes().beginTransaction(change.getId());
+ try {
+ change = db.changes().atomicUpdate(
+ change.getId(),
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus().isOpen()) {
+ change.setStatus(Change.Status.ABANDONED);
+ return change;
+ }
+ return null;
+ }
+ });
+ if (change != null) {
+ ChangeMessage msg = new ChangeMessage(
+ new ChangeMessage.Key(
+ change.getId(),
+ ChangeUtil.messageUUID(db)),
+ null,
+ change.getLastUpdatedOn(),
+ change.currentPatchSetId());
+ msg.setMessage("Project was deleted.");
+ db.changeMessages().insert(Collections.singleton(msg));
+ new ApprovalsUtil(db).syncChangeStatus(change);
+ db.commit();
+ indexer.index(change);
+ }
+ } finally {
+ db.rollback();
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 862eb2b..e9704e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -180,8 +180,8 @@
return submitter;
}
- public CodeReviewCommit createCherryPickFromCommit(Repository repo,
- ObjectInserter inserter, CodeReviewCommit mergeTip, CodeReviewCommit originalCommit,
+ public RevCommit createCherryPickFromCommit(Repository repo,
+ ObjectInserter inserter, RevCommit mergeTip, RevCommit originalCommit,
PersonIdent cherryPickCommitterIdent, String commitMsg, RevWalk rw)
throws MissingObjectException, IncorrectObjectTypeException, IOException {
@@ -189,20 +189,18 @@
m.setBase(originalCommit.getParent(0));
if (m.merge(mergeTip, originalCommit)) {
+ ObjectId tree = m.getResultTreeId();
+ if (tree.equals(mergeTip.getTree())) {
+ return null;
+ }
- final CommitBuilder mergeCommit = new CommitBuilder();
-
- mergeCommit.setTreeId(m.getResultTreeId());
+ CommitBuilder mergeCommit = new CommitBuilder();
+ mergeCommit.setTreeId(tree);
mergeCommit.setParentId(mergeTip);
mergeCommit.setAuthor(originalCommit.getAuthorIdent());
mergeCommit.setCommitter(cherryPickCommitterIdent);
mergeCommit.setMessage(commitMsg);
-
- final ObjectId id = commit(inserter, mergeCommit);
- final CodeReviewCommit newCommit =
- (CodeReviewCommit) rw.parseCommit(id);
-
- return newCommit;
+ return rw.parseCommit(commit(inserter, mergeCommit));
} else {
return null;
}
@@ -490,7 +488,7 @@
public ObjectInserter createDryRunInserter() {
return new ObjectInserter() {
private final MutableObjectId buf = new MutableObjectId();
- private final static int LAST_BYTE = Constants.OBJECT_ID_LENGTH - 1;
+ private static final int LAST_BYTE = Constants.OBJECT_ID_LENGTH - 1;
@Override
public ObjectId insert(int objectType, long length, InputStream in)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
index dc08a6c..43e0975 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -19,6 +19,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -35,30 +36,34 @@
private final InternalFactory factory;
private final GitRepositoryManager mgr;
private final PersonIdent serverIdent;
- private final PersonIdent userIdent;
+ private final Provider<IdentifiedUser> identifiedUser;
@Inject
User(InternalFactory factory, GitRepositoryManager mgr,
- @GerritPersonIdent PersonIdent serverIdent, IdentifiedUser currentUser) {
+ @GerritPersonIdent PersonIdent serverIdent,
+ Provider<IdentifiedUser> identifiedUser) {
this.factory = factory;
this.mgr = mgr;
this.serverIdent = serverIdent;
- this.userIdent = currentUser.newCommitterIdent( //
- serverIdent.getWhen(), //
- serverIdent.getTimeZone());
+ this.identifiedUser = identifiedUser;
}
public PersonIdent getUserPersonIdent() {
- return userIdent;
+ return createPersonIdent();
}
public MetaDataUpdate create(Project.NameKey name)
throws RepositoryNotFoundException, IOException {
MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
- md.getCommitBuilder().setAuthor(userIdent);
+ md.getCommitBuilder().setAuthor(createPersonIdent());
md.getCommitBuilder().setCommitter(serverIdent);
return md;
}
+
+ private PersonIdent createPersonIdent() {
+ return identifiedUser.get().newCommitterIdent(
+ serverIdent.getWhen(), serverIdent.getTimeZone());
+ }
}
public static class Server {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 9c5633b..f7b5ab0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -16,6 +16,8 @@
import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import com.google.common.base.Strings;
+
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.slf4j.Logger;
@@ -62,7 +64,7 @@
public class Task implements ProgressMonitor {
private final String name;
private final int total;
- private volatile int count;
+ private int count;
private int lastPercent;
Task(final String subTaskName, final int totalWork) {
@@ -73,29 +75,35 @@
/**
* Indicate that work has been completed on this sub-task.
* <p>
- * Must be called from the worker thread.
+ * Must be called from a worker thread.
*
* @param completed number of work units completed.
*/
@Override
public void update(final int completed) {
- count += completed;
- if (total != UNKNOWN) {
- int percent = count * 100 / total;
- if (percent > lastPercent) {
- lastPercent = percent;
- wakeUp();
+ boolean w = false;
+ synchronized (this) {
+ count += completed;
+ if (total != UNKNOWN) {
+ int percent = count * 100 / total;
+ if (percent > lastPercent) {
+ lastPercent = percent;
+ w = true;
+ }
}
}
+ if (w) {
+ wakeUp();
+ }
}
/**
* Indicate that this sub-task is finished.
* <p>
- * Must be called from the worker thread.
+ * Must be called from a worker thread.
*/
public void end() {
- if (total == UNKNOWN && count > 0) {
+ if (total == UNKNOWN && getCount() > 0) {
wakeUp();
}
}
@@ -116,6 +124,10 @@
public boolean isCancelled() {
return false;
}
+
+ public synchronized int getCount() {
+ return count;
+ }
}
private final OutputStream out;
@@ -165,19 +177,19 @@
/**
* Wait for a task managed by a {@link Future}.
* <p>
- * Must be called from the main thread, <em>not</em> the worker thread. Once
- * the worker thread calls {@link #end()}, the future has an additional
+ * Must be called from the main thread, <em>not</em> a worker thread. Once a
+ * worker thread calls {@link #end()}, the future has an additional
* <code>maxInterval</code> to finish before it is forcefully cancelled and
* {@link ExecutionException} is thrown.
*
- * @param workerFuture a future that returns when the worker thread is
- * finished.
+ * @param workerFuture a future that returns when worker threads are finished.
* @param timeoutTime overall timeout for the task; the future is forcefully
* cancelled if the task exceeds the timeout. Non-positive values indicate
* no timeout.
* @param timeoutUnit unit for overall task timeout.
- * @throws ExecutionException if this thread or the worker thread was
- * interrupted, the worker was cancelled, or the worker timed out.
+ * @throws ExecutionException if this thread or a worker thread was
+ * interrupted, the worker was cancelled, or timed out waiting for a
+ * worker to call {@link #end()}.
*/
public void waitFor(final Future<?> workerFuture, final long timeoutTime,
final TimeUnit timeoutUnit) throws ExecutionException {
@@ -268,7 +280,7 @@
/**
* End the overall task.
* <p>
- * Must be called from the worker thread.
+ * Must be called from a worker thread.
*/
public synchronized void end() {
done = true;
@@ -319,7 +331,10 @@
first = false;
}
- s.append(' ').append(t.name).append(": ");
+ s.append(' ');
+ if (!Strings.isNullOrEmpty(t.name)) {
+ s.append(t.name).append(": ");
+ }
if (t.total == UNKNOWN) {
s.append(count);
} else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index bfaa97e..3df559d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -59,6 +59,7 @@
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
@@ -107,6 +108,7 @@
private static final String RECEIVE = "receive";
private static final String KEY_REQUIRE_SIGNED_OFF_BY = "requireSignedOffBy";
private static final String KEY_REQUIRE_CHANGE_ID = "requireChangeId";
+ private static final String KEY_MAX_OBJECT_SIZE_LIMIT = "maxObjectSizeLimit";
private static final String KEY_REQUIRE_CONTRIBUTOR_AGREEMENT =
"requireContributorAgreement";
@@ -126,8 +128,9 @@
private static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
private static final String KEY_VALUE = "value";
private static final String KEY_CAN_OVERRIDE = "canOverride";
+ private static final String KEY_Branch = "branch";
private static final Set<String> LABEL_FUNCTIONS = ImmutableSet.of(
- "MaxWithBlock", "MaxNoBlock", "NoBlock", "NoOp");
+ "MaxWithBlock", "AnyWithBlock", "MaxNoBlock", "NoBlock", "NoOp");
private static final SubmitType defaultSubmitAction =
SubmitType.MERGE_IF_NECESSARY;
@@ -145,6 +148,7 @@
private List<CommentLinkInfo> commentLinkSections;
private List<ValidationError> validationErrors;
private ObjectId rulesId;
+ private long maxObjectSizeLimit;
public static ProjectConfig read(MetaDataUpdate update) throws IOException,
ConfigInvalidException {
@@ -319,6 +323,14 @@
}
/**
+ * @return the maxObjectSizeLimit for this project, if set. Zero if this
+ * project doesn't define own maxObjectSizeLimit.
+ */
+ public long getMaxObjectSizeLimit() {
+ return maxObjectSizeLimit;
+ }
+
+ /**
* Check all GroupReferences use current group name, repairing stale ones.
*
* @param groupBackend cache to use when looking up group information by UUID.
@@ -372,6 +384,7 @@
p.setUseContributorAgreements(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, Project.InheritableBoolean.INHERIT));
p.setUseSignedOffBy(getEnum(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, Project.InheritableBoolean.INHERIT));
p.setRequireChangeID(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, Project.InheritableBoolean.INHERIT));
+ p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, defaultSubmitAction));
p.setUseContentMerge(getEnum(rc, SUBMIT, null, KEY_MERGE_CONTENT, Project.InheritableBoolean.INHERIT));
@@ -386,6 +399,8 @@
loadNotifySections(rc, groupsByName);
loadLabelSections(rc);
loadCommentLinkSections(rc);
+
+ maxObjectSizeLimit = rc.getLong(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, 0);
}
private void loadAccountsSection(
@@ -512,7 +527,7 @@
if (isPermission(varName)) {
Permission perm = as.getPermission(varName, true);
loadPermissionRules(rc, ACCESS, refName, varName, groupsByName,
- perm, perm.isLabel());
+ perm, Permission.hasRange(varName));
}
}
}
@@ -520,15 +535,13 @@
AccessSection capability = null;
for (String varName : rc.getNames(CAPABILITY)) {
- if (GlobalCapability.isCapability(varName)) {
- if (capability == null) {
- capability = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
- accessSections.put(AccessSection.GLOBAL_CAPABILITIES, capability);
- }
- Permission perm = capability.getPermission(varName, true);
- loadPermissionRules(rc, CAPABILITY, null, varName, groupsByName, perm,
- GlobalCapability.hasRange(varName));
+ if (capability == null) {
+ capability = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
+ accessSections.put(AccessSection.GLOBAL_CAPABILITIES, capability);
}
+ Permission perm = capability.getPermission(varName, true);
+ loadPermissionRules(rc, CAPABILITY, null, varName, groupsByName, perm,
+ GlobalCapability.hasRange(varName));
}
}
@@ -640,10 +653,17 @@
rc.getBoolean(LABEL, name, KEY_COPY_MAX_SCORE, false));
label.setCanOverride(
rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE, true));
+ label.setRefPatterns(getStringListOrNull(rc, LABEL, name, KEY_Branch));
labelSections.put(name, label);
}
}
+ private List<String> getStringListOrNull(Config rc, String section,
+ String subSection, String name) {
+ String[] ac = rc.getStringList(section, subSection, name);
+ return ac.length == 0 ? null : Arrays.asList(ac);
+ }
+
private void loadCommentLinkSections(Config rc) {
Set<String> subsections = rc.getSubsections(COMMENTLINK);
commentLinkSections = Lists.newArrayListWithCapacity(subsections.size());
@@ -711,11 +731,12 @@
set(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, p.getUseContributorAgreements(), Project.InheritableBoolean.INHERIT);
set(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, p.getUseSignedOffBy(), Project.InheritableBoolean.INHERIT);
set(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, p.getRequireChangeID(), Project.InheritableBoolean.INHERIT);
+ set(rc, RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, validMaxObjectSizeLimit(p.getMaxObjectSizeLimit()));
set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction);
set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), Project.InheritableBoolean.INHERIT);
- set(rc, PROJECT, null, KEY_STATE, p.getState(), null);
+ set(rc, PROJECT, null, KEY_STATE, p.getState(), defaultStateValue);
set(rc, DASHBOARD, null, KEY_DEFAULT, p.getDefaultDashboard());
set(rc, DASHBOARD, null, KEY_LOCAL_DEFAULT, p.getLocalDefaultDashboard());
@@ -732,6 +753,35 @@
saveGroupList();
}
+ public static final String validMaxObjectSizeLimit(String value)
+ throws ConfigInvalidException {
+ if (value == null) {
+ return null;
+ }
+ value = value.trim();
+ if (value.isEmpty()) {
+ return null;
+ }
+ Config cfg = new Config();
+ cfg.fromText("[s]\nn=" + value);
+ try {
+ long s = cfg.getLong("s", "n", 0);
+ if (s < 0) {
+ throw new ConfigInvalidException(String.format(
+ "Negative value '%s' not allowed as %s", value,
+ KEY_MAX_OBJECT_SIZE_LIMIT));
+ }
+ if (s == 0) {
+ // return null for the default so that it is not persisted
+ return null;
+ }
+ return value;
+ } catch (IllegalArgumentException e) {
+ throw new ConfigInvalidException(
+ String.format("Value '%s' not parseable as a Long", value), e);
+ }
+ }
+
private void saveAccountsSection(Config rc, Set<AccountGroup.UUID> keepGroups) {
if (accountsSection != null) {
rc.setStringList(ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY,
@@ -836,8 +886,7 @@
rc.setStringList(CAPABILITY, null, permission.getName(), rules);
}
for (String varName : rc.getNames(CAPABILITY)) {
- if (GlobalCapability.isCapability(varName)
- && !have.contains(varName.toLowerCase())) {
+ if (!have.contains(varName.toLowerCase())) {
rc.unset(CAPABILITY, null, varName);
}
}
@@ -870,7 +919,7 @@
for (Permission permission : sort(as.getPermissions())) {
have.add(permission.getName().toLowerCase());
- boolean needRange = permission.isLabel();
+ boolean needRange = Permission.hasRange(permission.getName());
List<String> rules = new ArrayList<String>();
for (PermissionRule rule : sort(permission.getRules())) {
GroupReference group = rule.getGroup();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
index 8490ea1..8fec2fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
@@ -14,10 +14,13 @@
package com.google.gerrit.server.git;
+import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
+
import com.google.common.collect.Lists;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.changedetail.PathConflictException;
import com.google.gerrit.server.changedetail.RebaseChange;
import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -25,6 +28,7 @@
import com.google.gwtorm.server.OrmException;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
import java.io.IOException;
import java.util.HashMap;
@@ -35,12 +39,14 @@
private final RebaseChange rebaseChange;
private final Map<Change.Id, CodeReviewCommit> newCommits;
+ private final PersonIdent committerIdent;
RebaseIfNecessary(final SubmitStrategy.Arguments args,
- final RebaseChange rebaseChange) {
+ final RebaseChange rebaseChange, PersonIdent committerIdent) {
super(args);
this.rebaseChange = rebaseChange;
this.newCommits = new HashMap<Change.Id, CodeReviewCommit>();
+ this.committerIdent = committerIdent;
}
@Override
@@ -73,11 +79,14 @@
} else {
try {
+ final IdentifiedUser uploader =
+ args.identifiedUserFactory.create(
+ args.mergeUtil.getSubmitter(n.patchsetId).getAccountId());
final PatchSet newPatchSet =
rebaseChange.rebase(args.repo, args.rw, args.inserter,
- n.patchsetId, n.change,
- args.mergeUtil.getSubmitter(n.patchsetId).getAccountId(),
- newMergeTip, args.mergeUtil);
+ n.patchsetId, n.change, uploader,
+ newMergeTip, args.mergeUtil, committerIdent,
+ false, false, ValidatePolicy.NONE);
List<PatchSetApproval> approvals = Lists.newArrayList();
for (PatchSetApproval a : args.mergeUtil.getApprovalsForCommit(n)) {
approvals.add(new PatchSetApproval(newPatchSet.getId(), a));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 3e28b07..5245957 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.git;
-import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals;
import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
@@ -62,6 +61,7 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeResource;
@@ -76,6 +76,7 @@
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.mail.CreateChangeSender;
import com.google.gerrit.server.mail.MailUtil.MailRecipients;
import com.google.gerrit.server.mail.MergedSender;
@@ -261,10 +262,12 @@
private final CommitValidators.Factory commitValidatorsFactory;
private final TrackingFooters trackingFooters;
private final TagCache tagCache;
- private final ChangeInserter changeInserter;
+ private final AccountCache accountCache;
+ private final ChangeInserter.Factory changeInserterFactory;
private final WorkQueue workQueue;
private final ListeningExecutorService changeUpdateExector;
private final RequestScopePropagator requestScopePropagator;
+ private final ChangeIndexer indexer;
private final SshInfo sshInfo;
private final AllProjectsName allProjectsName;
private final ReceiveConfig receiveConfig;
@@ -316,8 +319,9 @@
final ProjectCache projectCache,
final GitRepositoryManager repoManager,
final TagCache tagCache,
+ final AccountCache accountCache,
final ChangeCache changeCache,
- final ChangeInserter changeInserter,
+ final ChangeInserter.Factory changeInserterFactory,
final CommitValidators.Factory commitValidatorsFactory,
@CanonicalWebUrl @Nullable final String canonicalWebUrl,
@GerritPersonIdent final PersonIdent gerritIdent,
@@ -325,6 +329,7 @@
final WorkQueue workQueue,
@ChangeUpdateExecutor ListeningExecutorService changeUpdateExector,
final RequestScopePropagator requestScopePropagator,
+ final ChangeIndexer indexer,
final SshInfo sshInfo,
final AllProjectsName allProjectsName,
ReceiveConfig config,
@@ -350,11 +355,13 @@
this.canonicalWebUrl = canonicalWebUrl;
this.trackingFooters = trackingFooters;
this.tagCache = tagCache;
- this.changeInserter = changeInserter;
+ this.accountCache = accountCache;
+ this.changeInserterFactory = changeInserterFactory;
this.commitValidatorsFactory = commitValidatorsFactory;
this.workQueue = workQueue;
this.changeUpdateExector = changeUpdateExector;
this.requestScopePropagator = requestScopePropagator;
+ this.indexer = indexer;
this.sshInfo = sshInfo;
this.allProjectsName = allProjectsName;
this.receiveConfig = config;
@@ -397,7 +404,8 @@
}
});
advHooks.add(rp.getAdvertiseRefsHook());
- advHooks.add(new ReceiveCommitsAdvertiseRefsHook());
+ advHooks.add(new ReceiveCommitsAdvertiseRefsHook(
+ db, projectControl.getProject().getNameKey()));
rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
}
@@ -432,80 +440,6 @@
return rp;
}
- /** Scan part of history and include it in the advertisement. */
- public void advertiseHistory() {
- Set<ObjectId> toInclude = new HashSet<ObjectId>();
-
- // Advertise some recent open changes, in case a commit is based one.
- try {
- Set<PatchSet.Id> toGet = new HashSet<PatchSet.Id>();
- for (Change change : db.changes()
- .byProjectOpenNext(project.getNameKey(), "z", 32)) {
- PatchSet.Id id = change.currentPatchSetId();
- if (id != null) {
- toGet.add(id);
- }
- }
- for (PatchSet ps : db.patchSets().get(toGet)) {
- if (ps.getRevision() != null && ps.getRevision().get() != null) {
- toInclude.add(ObjectId.fromString(ps.getRevision().get()));
- }
- }
- } catch (OrmException err) {
- log.error("Cannot list open changes of " + project.getNameKey(), err);
- }
-
- // Size of an additional ".have" line.
- final int haveLineLen = 4 + Constants.OBJECT_ID_STRING_LENGTH + 1 + 5 + 1;
-
- // Maximum number of bytes to "waste" in the advertisement with
- // a peek at this repository's current reachable history.
- final int maxExtraSize = 8192;
-
- // Number of recent commits to advertise immediately, hoping to
- // show a client a nearby merge base.
- final int base = 64;
-
- // Number of commits to skip once base has already been shown.
- final int step = 16;
-
- // Total number of commits to extract from the history.
- final int max = maxExtraSize / haveLineLen;
-
- // Scan history until the advertisement is full.
- Set<ObjectId> alreadySending = rp.getAdvertisedObjects();
- RevWalk rw = rp.getRevWalk();
- for (ObjectId haveId : alreadySending) {
- try {
- rw.markStart(rw.parseCommit(haveId));
- } catch (IOException badCommit) {
- continue;
- }
- }
-
- int stepCnt = 0;
- RevCommit c;
- try {
- while ((c = rw.next()) != null && toInclude.size() < max) {
- if (alreadySending.contains(c)) {
- } else if (toInclude.contains(c)) {
- } else if (c.getParentCount() > 1) {
- } else if (toInclude.size() < base) {
- toInclude.add(c);
- } else {
- stepCnt = ++stepCnt % step;
- if (stepCnt == 0) {
- toInclude.add(c);
- }
- }
- }
- } catch (IOException err) {
- log.error("Error trying to advertise history on " + project.getNameKey(), err);
- }
- rw.reset();
- rp.getAdvertisedObjects().addAll(toInclude);
- }
-
/** Determine if the user can upload commits. */
public Capable canUpload() {
Capable result = projectControl.canPushToAtLeastOneRef();
@@ -1143,6 +1077,10 @@
magicBranch.dest = new Branch.NameKey(project.getNameKey(), ref);
magicBranch.ctl = projectControl.controlForRef(ref);
+ if (!magicBranch.ctl.canWrite()) {
+ reject(cmd, "project is read only");
+ return;
+ }
if (!magicBranch.ctl.canUpload()) {
errors.put(Error.CODE_REVIEW, ref);
reject(cmd, "cannot upload review");
@@ -1344,7 +1282,7 @@
Change.Key changeKey = new Change.Key("I" + c.name());
final List<String> idList = c.getFooterLines(CHANGE_ID);
if (idList.isEmpty()) {
- newChanges.add(new CreateRequest(c, changeKey));
+ newChanges.add(new CreateRequest(magicBranch.ctl, c, changeKey));
continue;
}
@@ -1394,7 +1332,7 @@
newChangeIds.add(p.changeKey);
}
- newChanges.add(new CreateRequest(p.commit, p.changeKey));
+ newChanges.add(new CreateRequest(magicBranch.ctl, p.commit, p.changeKey));
}
} catch (IOException e) {
// Should never happen, the core receive process would have
@@ -1460,34 +1398,22 @@
private class CreateRequest {
final RevCommit commit;
final Change change;
- final PatchSet ps;
final ReceiveCommand cmd;
- private final PatchSetInfo info;
+ final ChangeInserter ins;
boolean created;
- CreateRequest(RevCommit c, Change.Key changeKey) throws OrmException {
+ CreateRequest(RefControl ctl, RevCommit c, Change.Key changeKey)
+ throws OrmException {
commit = c;
-
change = new Change(changeKey,
new Change.Id(db.nextChangeId()),
currentUser.getAccountId(),
magicBranch.dest);
change.setTopic(magicBranch.topic);
-
- ps = new PatchSet(new PatchSet.Id(change.getId(), INITIAL_PATCH_SET_ID));
- ps.setCreatedOn(change.getCreatedOn());
- ps.setUploader(change.getOwner());
- ps.setRevision(toRevId(c));
-
- if (magicBranch.isDraft()) {
- change.setStatus(Change.Status.DRAFT);
- ps.setDraft(true);
- }
-
- info = patchSetInfoFactory.get(c, ps.getId());
- change.setCurrentPatchSet(info);
- ChangeUtil.updated(change);
- cmd = new ReceiveCommand(ObjectId.zeroId(), c, ps.getRefName());
+ ins = changeInserterFactory.create(ctl, change, c)
+ .setDraft(magicBranch.isDraft());
+ cmd = new ReceiveCommand(ObjectId.zeroId(), c,
+ ins.getPatchSet().getRefName());
}
CheckedFuture<Void, OrmException> insertChange() throws IOException {
@@ -1518,6 +1444,7 @@
}
private void insertChange(ReviewDb db) throws OrmException {
+ final PatchSet ps = ins.getPatchSet();
final Account.Id me = currentUser.getAccountId();
final List<FooterLine> footerLines = commit.getFooterLines();
final MailRecipients recipients = new MailRecipients();
@@ -1527,9 +1454,10 @@
recipients.add(getRecipientsFromFooters(accountResolver, ps, footerLines));
recipients.remove(me);
- changeInserter.insertChange(db, change, ps, commit, labelTypes, info,
- recipients.getReviewers());
-
+ ins
+ .setReviewers(recipients.getReviewers())
+ .setSendMail(false)
+ .insert();
created = true;
workQueue.getDefaultQueue()
@@ -1540,7 +1468,7 @@
CreateChangeSender cm =
createChangeSenderFactory.create(change);
cm.setFrom(me);
- cm.setPatchSet(ps, info);
+ cm.setPatchSet(ps, ins.getPatchSetInfo());
cm.addReviewers(recipients.getReviewers());
cm.addExtraCC(recipients.getCcOnly());
cm.send();
@@ -1749,12 +1677,16 @@
eq(newCommit.getFullMessage(), priorCommit.getFullMessage());
final boolean parentsEq = parentsEqual(newCommit, priorCommit);
final boolean authorEq = authorEqual(newCommit, priorCommit);
+ final ObjectReader reader = rp.getRevWalk().getObjectReader();
if (messageEq && parentsEq && authorEq && !autoClose) {
+ addMessage(String.format(
+ "(W) No changes between prior commit %s and new commit %s",
+ reader.abbreviate(priorCommit).name(),
+ reader.abbreviate(newCommit).name()));
reject(inputCommand, "no changes made");
return false;
} else {
- ObjectReader reader = rp.getRevWalk().getObjectReader();
StringBuilder msg = new StringBuilder();
msg.append("(W) ");
msg.append(reader.abbreviate(newCommit).name());
@@ -1924,6 +1856,7 @@
if (cmd.getResult() == NOT_ATTEMPTED) {
cmd.execute(rp);
}
+ indexer.index(change);
gitRefUpdated.fire(project.getNameKey(), newPatchSet.getRefName(),
ObjectId.zeroId(), newCommit);
hooks.doPatchsetCreatedHook(change, newPatchSet, db);
@@ -2057,6 +1990,7 @@
return;
}
+ boolean defaultName = Strings.isNullOrEmpty(currentUser.getAccount().getFullName());
final RevWalk walk = rp.getRevWalk();
walk.reset();
walk.sort(RevSort.NONE);
@@ -2072,6 +2006,23 @@
} else if (!validCommit(ctl, cmd, c)) {
break;
}
+
+ if (defaultName && currentUser.getEmailAddresses().contains(
+ c.getCommitterIdent().getEmailAddress())) {
+ try {
+ Account a = db.accounts().get(currentUser.getAccountId());
+ if (a != null && Strings.isNullOrEmpty(a.getFullName())) {
+ a.setFullName(c.getCommitterIdent().getName());
+ db.accounts().update(Collections.singleton(a));
+ currentUser.getAccount().setFullName(a.getFullName());
+ accountCache.evict(a.getId());
+ }
+ } catch (OrmException e) {
+ log.warn("Cannot default full_name", e);
+ } finally {
+ defaultName = false;
+ }
+ }
}
} catch (IOException err) {
cmd.setResult(REJECTED_MISSING_OBJECT);
@@ -2195,6 +2146,7 @@
ReplaceRequest result = new ReplaceRequest(cid, commit, cmd, false);
result.change = change;
+ result.changeCtl = projectControl.controlFor(change);
result.newPatchSet = ps;
result.info = patchSetInfoFactory.get(commit, psi);
result.mergedIntoRef = refName;
@@ -2228,7 +2180,7 @@
private void markChangeMergedByPush(final ReviewDb db,
final ReplaceRequest result) throws OrmException {
- final Change change = result.change;
+ Change change = result.change;
final String mergedIntoRef = result.mergedIntoRef;
change.setCurrentPatchSet(result.info);
@@ -2256,17 +2208,19 @@
db.changeMessages().insert(Collections.singleton(msg));
- db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- if (change.getStatus().isOpen()) {
- change.setCurrentPatchSet(result.info);
- change.setStatus(Change.Status.MERGED);
- ChangeUtil.updated(change);
- }
- return change;
- }
- });
+ change = db.changes().atomicUpdate(
+ change.getId(), new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus().isOpen()) {
+ change.setCurrentPatchSet(result.info);
+ change.setStatus(Change.Status.MERGED);
+ ChangeUtil.updated(change);
+ }
+ return change;
+ }
+ });
+ indexer.index(change);
}
private void sendMergedEmail(final ReplaceRequest result) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
index 0eb9b61..ace4e90 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -15,17 +15,43 @@
package com.google.gerrit.server.git;
import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.util.MagicBranch;
+import com.google.gwtorm.server.OrmException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.AdvertiseRefsHook;
import org.eclipse.jgit.transport.BaseReceivePack;
import org.eclipse.jgit.transport.UploadPack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.io.IOException;
import java.util.Map;
+import java.util.Set;
/** Exposes only the non refs/changes/ reference names. */
public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
+ private static final Logger log = LoggerFactory
+ .getLogger(ReceiveCommitsAdvertiseRefsHook.class);
+
+ private final ReviewDb db;
+ private final Project.NameKey projectName;
+
+ public ReceiveCommitsAdvertiseRefsHook(ReviewDb db,
+ Project.NameKey projectName) {
+ this.db = db;
+ this.projectName = projectName;
+ }
+
@Override
public void advertiseRefs(UploadPack us) {
throw new UnsupportedOperationException(
@@ -45,7 +71,84 @@
r.put(name, e.getValue());
}
}
- rp.setAdvertisedRefs(r, rp.getAdvertisedObjects());
+ rp.setAdvertisedRefs(r, advertiseHistory(r.values(), rp));
+ }
+
+ private Set<ObjectId> advertiseHistory(
+ Iterable<Ref> sending,
+ BaseReceivePack rp) {
+ Set<ObjectId> toInclude = Sets.newHashSet();
+
+ // Advertise some recent open changes, in case a commit is based one.
+ try {
+ Set<PatchSet.Id> toGet = Sets.newHashSetWithExpectedSize(32);
+ for (Change c : db.changes().byProjectOpenNext(projectName, "z", 32)) {
+ PatchSet.Id id = c.currentPatchSetId();
+ if (id != null) {
+ toGet.add(id);
+ }
+ }
+ for (PatchSet ps : db.patchSets().get(toGet)) {
+ if (ps.getRevision() != null && ps.getRevision().get() != null) {
+ toInclude.add(ObjectId.fromString(ps.getRevision().get()));
+ }
+ }
+ } catch (OrmException err) {
+ log.error("Cannot list open changes of " + projectName, err);
+ }
+
+ // Size of an additional ".have" line.
+ final int haveLineLen = 4 + Constants.OBJECT_ID_STRING_LENGTH + 1 + 5 + 1;
+
+ // Maximum number of bytes to "waste" in the advertisement with
+ // a peek at this repository's current reachable history.
+ final int maxExtraSize = 8192;
+
+ // Number of recent commits to advertise immediately, hoping to
+ // show a client a nearby merge base.
+ final int base = 64;
+
+ // Number of commits to skip once base has already been shown.
+ final int step = 16;
+
+ // Total number of commits to extract from the history.
+ final int max = maxExtraSize / haveLineLen;
+
+ // Scan history until the advertisement is full.
+ Set<ObjectId> alreadySending = Sets.newHashSet();
+ RevWalk rw = rp.getRevWalk();
+ for (Ref ref : sending) {
+ try {
+ if (ref.getObjectId() != null) {
+ alreadySending.add(ref.getObjectId());
+ rw.markStart(rw.parseCommit(ref.getObjectId()));
+ }
+ } catch (IOException badCommit) {
+ continue;
+ }
+ }
+
+ int stepCnt = 0;
+ RevCommit c;
+ try {
+ while ((c = rw.next()) != null && toInclude.size() < max) {
+ if (alreadySending.contains(c)) {
+ } else if (toInclude.contains(c)) {
+ } else if (c.getParentCount() > 1) {
+ } else if (toInclude.size() < base) {
+ toInclude.add(c);
+ } else {
+ stepCnt = ++stepCnt % step;
+ if (stepCnt == 0) {
+ toInclude.add(c);
+ }
+ }
+ }
+ } catch (IOException err) {
+ log.error("Error trying to advertise history on " + projectName, err);
+ }
+ rw.reset();
+ return toInclude;
}
private static boolean skip(String name) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
index 7c2ba86..9626e23 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
@@ -20,6 +20,7 @@
import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.index.ChangeIndexer;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
@@ -55,13 +56,15 @@
protected final Set<RevCommit> alreadyAccepted;
protected final Branch.NameKey destBranch;
protected final MergeUtil mergeUtil;
+ protected final ChangeIndexer indexer;
protected final MergeSorter mergeSorter;
Arguments(final IdentifiedUser.GenericFactory identifiedUserFactory,
final PersonIdent myIdent, final ReviewDb db, final Repository repo,
final RevWalk rw, final ObjectInserter inserter,
final RevFlag canMergeFlag, final Set<RevCommit> alreadyAccepted,
- final Branch.NameKey destBranch, final MergeUtil mergeUtil) {
+ final Branch.NameKey destBranch, final MergeUtil mergeUtil,
+ final ChangeIndexer indexer) {
this.identifiedUserFactory = identifiedUserFactory;
this.myIdent = myIdent;
this.db = db;
@@ -73,6 +76,7 @@
this.alreadyAccepted = alreadyAccepted;
this.destBranch = destBranch;
this.mergeUtil = mergeUtil;
+ this.indexer = indexer;
this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
index 8bf831c..845139d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
@@ -22,6 +22,7 @@
import com.google.gerrit.server.changedetail.RebaseChange;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
@@ -54,6 +55,7 @@
private final RebaseChange rebaseChange;
private final ProjectCache projectCache;
private final MergeUtil.Factory mergeUtilFactory;
+ private final ChangeIndexer indexer;
@Inject
SubmitStrategyFactory(
@@ -63,7 +65,8 @@
@CanonicalWebUrl @Nullable final Provider<String> urlProvider,
final GitReferenceUpdated gitRefUpdated, final RebaseChange rebaseChange,
final ProjectCache projectCache,
- final MergeUtil.Factory mergeUtilFactory) {
+ final MergeUtil.Factory mergeUtilFactory,
+ final ChangeIndexer indexer) {
this.identifiedUserFactory = identifiedUserFactory;
this.myIdent = myIdent;
this.patchSetInfoFactory = patchSetInfoFactory;
@@ -71,6 +74,7 @@
this.rebaseChange = rebaseChange;
this.projectCache = projectCache;
this.mergeUtilFactory = mergeUtilFactory;
+ this.indexer = indexer;
}
public SubmitStrategy create(final SubmitType submitType, final ReviewDb db,
@@ -82,7 +86,7 @@
final SubmitStrategy.Arguments args =
new SubmitStrategy.Arguments(identifiedUserFactory, myIdent, db, repo,
rw, inserter, canMergeFlag, alreadyAccepted, destBranch,
- mergeUtilFactory.create(project));
+ mergeUtilFactory.create(project), indexer);
switch (submitType) {
case CHERRY_PICK:
return new CherryPick(args, patchSetInfoFactory, gitRefUpdated);
@@ -93,7 +97,7 @@
case MERGE_IF_NECESSARY:
return new MergeIfNecessary(args);
case REBASE_IF_NECESSARY:
- return new RebaseIfNecessary(args, rebaseChange);
+ return new RebaseIfNecessary(args, rebaseChange, myIdent);
default:
final String errorMsg = "No submit strategy for: " + submitType;
log.error(errorMsg);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
index 3c64229..dec1768 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
@@ -10,7 +10,7 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
-// limitations under the License.package com.google.gerrit.server.git;
+// limitations under the License.
package com.google.gerrit.server.git;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
index 7d95db2..b63378f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
@@ -10,7 +10,7 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
-// limitations under the License.package com.google.gerrit.server.git;
+// limitations under the License.
package com.google.gerrit.server.git;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
index c57942c..f469059 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
@@ -10,7 +10,7 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
-// limitations under the License.package com.google.gerrit.server.git;
+// limitations under the License.
package com.google.gerrit.server.git;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
index d5120e0..d923e51 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
@@ -10,7 +10,7 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
-// limitations under the License.package com.google.gerrit.server.git;
+// limitations under the License.
package com.google.gerrit.server.git;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java
index af404b5..8be0a10 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java
@@ -16,6 +16,7 @@
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -29,12 +30,14 @@
private final int timeout;
private final PackConfig packConfig;
private final long maxObjectSizeLimit;
+ private final String maxObjectSizeLimitFormatted;
@Inject
TransferConfig(@GerritServerConfig final Config cfg) {
timeout = (int) ConfigUtil.getTimeUnit(cfg, "transfer", null, "timeout", //
0, TimeUnit.SECONDS);
maxObjectSizeLimit = cfg.getLong("receive", "maxObjectSizeLimit", 0);
+ maxObjectSizeLimitFormatted = cfg.getString("receive", null, "maxObjectSizeLimit");
packConfig = new PackConfig();
packConfig.setDeltaCompress(false);
@@ -54,4 +57,19 @@
public long getMaxObjectSizeLimit() {
return maxObjectSizeLimit;
}
+
+ public String getFormattedMaxObjectSizeLimit() {
+ return maxObjectSizeLimitFormatted;
+ }
+
+ public long getEffectiveMaxObjectSizeLimit(ProjectState p) {
+ long global = getMaxObjectSizeLimit();
+ long local = p.getMaxObjectSizeLimit();
+ if (global > 0 && local > 0) {
+ return Math.min(global, local);
+ } else {
+ // zero means "no limit", in this case the max is more limiting
+ return Math.max(global, local);
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
index bb11e62..64210a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -26,6 +26,7 @@
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.ArrayList;
+import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
@@ -248,6 +249,7 @@
private final Executor executor;
private final int taskId;
private final AtomicBoolean running;
+ private final Date startTime;
Task(Runnable runnable, RunnableScheduledFuture<V> task, Executor executor,
int taskId) {
@@ -256,6 +258,7 @@
this.executor = executor;
this.taskId = taskId;
this.running = new AtomicBoolean();
+ this.startTime = new Date();
}
public int getTaskId() {
@@ -281,6 +284,10 @@
return State.OTHER;
}
+ public Date getStartTime() {
+ return startTime;
+ }
+
public boolean cancel(boolean mayInterruptIfRunning) {
if (task.cancel(mayInterruptIfRunning)) {
// Tiny abuse of running: if the task needs to know it was
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index a57f923..e7b9d31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -14,11 +14,13 @@
package com.google.gerrit.server.git.validators;
+import com.google.common.base.CharMatcher;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ProjectConfig;
@@ -34,6 +36,7 @@
import com.jcraft.jsch.HostKey;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.FooterKey;
@@ -57,6 +60,9 @@
private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
+ private static final String GIT_HOOKS_COMMIT_MSG =
+ "`git rev-parse --git-dir`/hooks/commit-msg";
+
public interface Factory {
CommitValidators create(RefControl refControl, SshInfo sshInfo,
Repository repo);
@@ -65,6 +71,7 @@
private final PersonIdent gerritIdent;
private final RefControl refControl;
private final String canonicalWebUrl;
+ private final String installCommitMsgHookCommand;
private final SshInfo sshInfo;
private final Repository repo;
private final DynamicSet<CommitValidationListener> commitValidationListeners;
@@ -72,12 +79,15 @@
@Inject
CommitValidators(@GerritPersonIdent final PersonIdent gerritIdent,
@CanonicalWebUrl @Nullable final String canonicalWebUrl,
+ @GerritServerConfig final Config config,
final DynamicSet<CommitValidationListener> commitValidationListeners,
@Assisted final SshInfo sshInfo,
@Assisted final Repository repo, @Assisted final RefControl refControl) {
this.gerritIdent = gerritIdent;
this.refControl = refControl;
this.canonicalWebUrl = canonicalWebUrl;
+ this.installCommitMsgHookCommand =
+ config.getString("gerrit", null, "installCommitMsgHookCommand");
this.sshInfo = sshInfo;
this.repo = repo;
this.commitValidationListeners = commitValidationListeners;
@@ -98,7 +108,8 @@
if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
|| ReceiveCommits.NEW_PATCHSET.matcher(
receiveEvent.command.getRefName()).matches()) {
- validators.add(new ChangeIdValidator(refControl, canonicalWebUrl, sshInfo));
+ validators.add(new ChangeIdValidator(refControl, canonicalWebUrl,
+ installCommitMsgHookCommand, sshInfo));
}
validators.add(new ConfigValidator(refControl, repo));
validators.add(new PluginCommitValidationListener(commitValidationListeners));
@@ -132,7 +143,8 @@
if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
|| ReceiveCommits.NEW_PATCHSET.matcher(
receiveEvent.command.getRefName()).matches()) {
- validators.add(new ChangeIdValidator(refControl, canonicalWebUrl, sshInfo));
+ validators.add(new ChangeIdValidator(refControl, canonicalWebUrl,
+ installCommitMsgHookCommand, sshInfo));
}
validators.add(new ConfigValidator(refControl, repo));
validators.add(new PluginCommitValidationListener(commitValidationListeners));
@@ -153,23 +165,24 @@
}
public static class ChangeIdValidator implements CommitValidationListener {
- private final RefControl refControl;
+ private final ProjectControl projectControl;
private final String canonicalWebUrl;
+ private final String installCommitMsgHookCommand;
private final SshInfo sshInfo;
+ private final IdentifiedUser user;
public ChangeIdValidator(RefControl refControl, String canonicalWebUrl,
- SshInfo sshInfo) {
- this.refControl = refControl;
+ String installCommitMsgHookCommand, SshInfo sshInfo) {
+ this.projectControl = refControl.getProjectControl();
this.canonicalWebUrl = canonicalWebUrl;
+ this.installCommitMsgHookCommand = installCommitMsgHookCommand;
this.sshInfo = sshInfo;
+ this.user = (IdentifiedUser) projectControl.getCurrentUser();
}
@Override
public List<CommitValidationMessage> onCommitReceived(
CommitReceivedEvent receiveEvent) throws CommitValidationException {
-
- final ProjectControl projectControl = refControl.getProjectControl();
- IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
final List<String> idList = receiveEvent.commit.getFooterLines(CHANGE_ID);
List<CommitValidationMessage> messages =
@@ -178,8 +191,8 @@
if (idList.isEmpty()) {
if (projectControl.getProjectState().isRequireChangeID()) {
String errMsg = "missing Change-Id in commit message footer";
- messages.add(getFixedCommitMsgWithChangeId(errMsg, receiveEvent.commit,
- currentUser, canonicalWebUrl, sshInfo));
+ messages.add(getFixedCommitMsgWithChangeId(
+ errMsg, receiveEvent.commit));
throw new CommitValidationException(errMsg, messages);
}
} else if (idList.size() > 1) {
@@ -190,13 +203,99 @@
if (!v.matches("^I[0-9a-f]{8,}.*$")) {
final String errMsg =
"missing or invalid Change-Id line format in commit message footer";
- messages.add(getFixedCommitMsgWithChangeId(errMsg, receiveEvent.commit,
- currentUser, canonicalWebUrl, sshInfo));
+ messages.add(
+ getFixedCommitMsgWithChangeId(errMsg, receiveEvent.commit));
throw new CommitValidationException(errMsg, messages);
}
}
return Collections.<CommitValidationMessage>emptyList();
}
+
+ /**
+ * We handle 3 cases:
+ * 1. No change id in the commit message at all.
+ * 2. Change id last in the commit message but missing empty line to create the footer.
+ * 3. There is a change-id somewhere in the commit message, but we ignore it.
+ *
+ * @return The fixed up commit message
+ */
+ private CommitValidationMessage getFixedCommitMsgWithChangeId(
+ final String errMsg, final RevCommit c) {
+ final String changeId = "Change-Id:";
+ StringBuilder sb = new StringBuilder();
+ sb.append("ERROR: ").append(errMsg);
+ sb.append('\n');
+ sb.append("Suggestion for commit message:\n");
+
+ if (c.getFullMessage().indexOf(changeId) == -1) {
+ sb.append(c.getFullMessage());
+ sb.append('\n');
+ sb.append(changeId).append(" I").append(c.name());
+ } else {
+ String lines[] = c.getFullMessage().trim().split("\n");
+ String lastLine = lines.length > 0 ? lines[lines.length - 1] : "";
+
+ if (lastLine.indexOf(changeId) == 0) {
+ for (int i = 0; i < lines.length - 1; i++) {
+ sb.append(lines[i]);
+ sb.append('\n');
+ }
+
+ sb.append('\n');
+ sb.append(lastLine);
+ } else {
+ sb.append(c.getFullMessage());
+ sb.append('\n');
+ sb.append(changeId).append(" I").append(c.name());
+ sb.append('\n');
+ sb.append("Hint: A potential Change-Id was found, but it was not in the ");
+ sb.append("footer (last paragraph) of the commit message.");
+ }
+ }
+ sb.append('\n');
+ sb.append('\n');
+ sb.append("Hint: To automatically insert Change-Id, install the hook:\n");
+ sb.append(getCommitMessageHookInstallationHint()).append('\n');
+ sb.append('\n');
+
+ return new CommitValidationMessage(sb.toString(), false);
+ }
+
+ private String getCommitMessageHookInstallationHint() {
+ if (installCommitMsgHookCommand != null) {
+ return installCommitMsgHookCommand;
+ }
+ final List<HostKey> hostKeys = sshInfo.getHostKeys();
+
+ // If there are no SSH keys, the commit-msg hook must be installed via
+ // HTTP(S)
+ String p = GIT_HOOKS_COMMIT_MSG;
+ if (hostKeys.isEmpty()) {
+ return String.format(
+ " curl -Lo %s %s/tools/hooks/commit-msg ; chmod +x %s", p,
+ getGerritUrl(canonicalWebUrl), p);
+ }
+
+ // SSH keys exist, so the hook can be installed with scp.
+ String sshHost;
+ int sshPort;
+ String host = hostKeys.get(0).getHost();
+ int c = host.lastIndexOf(':');
+ if (0 <= c) {
+ if (host.startsWith("*:")) {
+ sshHost = getGerritHost(canonicalWebUrl);
+ } else {
+ sshHost = host.substring(0, c);
+ }
+ sshPort = Integer.parseInt(host.substring(c + 1));
+ } else {
+ sshHost = host;
+ sshPort = 22;
+ }
+
+ return String.format(" scp -p -P %d %s@%s:hooks/commit-msg %s",
+ sshPort, user.getUserName(), sshHost, p);
+ }
}
/**
@@ -448,93 +547,6 @@
}
/**
- * We handle 3 cases:
- * 1. No change id in the commit message at all.
- * 2. Change id last in the commit message but missing empty line to create the footer.
- * 3. There is a change-id somewhere in the commit message, but we ignore it.
- *
- * @return The fixed up commit message
- */
- private static CommitValidationMessage getFixedCommitMsgWithChangeId(final String errMsg,
- final RevCommit c, final IdentifiedUser currentUser,
- String canonicalWebUrl, final SshInfo sshInfo) {
- final String changeId = "Change-Id:";
- StringBuilder sb = new StringBuilder();
- sb.append("ERROR: ").append(errMsg);
- sb.append('\n');
- sb.append("Suggestion for commit message:\n");
-
- if (c.getFullMessage().indexOf(changeId) == -1) {
- sb.append(c.getFullMessage());
- sb.append('\n');
- sb.append(changeId).append(" I").append(c.name());
- } else {
- String lines[] = c.getFullMessage().trim().split("\n");
- String lastLine = lines.length > 0 ? lines[lines.length - 1] : "";
-
- if (lastLine.indexOf(changeId) == 0) {
- for (int i = 0; i < lines.length - 1; i++) {
- sb.append(lines[i]);
- sb.append('\n');
- }
-
- sb.append('\n');
- sb.append(lastLine);
- } else {
- sb.append(c.getFullMessage());
- sb.append('\n');
- sb.append(changeId).append(" I").append(c.name());
- sb.append('\n');
- sb.append("Hint: A potential Change-Id was found, but it was not in the ");
- sb.append("footer (last paragraph) of the commit message.");
- }
- }
- sb.append('\n');
- sb.append('\n');
- sb.append("Hint: To automatically insert Change-Id, install the hook:\n");
- sb.append(getCommitMessageHookInstallationHint(currentUser,
- canonicalWebUrl, sshInfo)).append('\n');
- sb.append('\n');
-
- return new CommitValidationMessage(sb.toString(), false);
- }
-
- private static String getCommitMessageHookInstallationHint(
- final IdentifiedUser currentUser, String canonicalWebUrl,
- final SshInfo sshInfo) {
- final List<HostKey> hostKeys = sshInfo.getHostKeys();
-
- // If there are no SSH keys, the commit-msg hook must be installed via
- // HTTP(S)
- if (hostKeys.isEmpty()) {
- String p = ".git/hooks/commit-msg";
- return String.format(
- " curl -o %s %s/tools/hooks/commit-msg ; chmod +x %s", p,
- getGerritUrl(canonicalWebUrl), p);
- }
-
- // SSH keys exist, so the hook can be installed with scp.
- String sshHost;
- int sshPort;
- String host = hostKeys.get(0).getHost();
- int c = host.lastIndexOf(':');
- if (0 <= c) {
- if (host.startsWith("*:")) {
- sshHost = getGerritHost(canonicalWebUrl);
- } else {
- sshHost = host.substring(0, c);
- }
- sshPort = Integer.parseInt(host.substring(c + 1));
- } else {
- sshHost = host;
- sshPort = 22;
- }
-
- return String.format(" scp -p -P %d %s@%s:hooks/commit-msg .git/hooks/",
- sshPort, currentUser.getUserName(), sshHost);
- }
-
- /**
* Get the Gerrit URL.
*
* @return the canonical URL (with any trailing slash removed) if it is
@@ -543,10 +555,7 @@
*/
private static String getGerritUrl(String canonicalWebUrl) {
if (canonicalWebUrl != null) {
- if (canonicalWebUrl.endsWith("/")) {
- return canonicalWebUrl.substring(0, canonicalWebUrl.lastIndexOf("/"));
- }
- return canonicalWebUrl;
+ return CharMatcher.is('/').trimTrailingFrom(canonicalWebUrl);
} else {
return "http://" + getGerritHost(canonicalWebUrl);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationException.java
new file mode 100644
index 0000000..78819a8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationException.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.validators;
+
+import com.google.gerrit.server.git.CommitMergeStatus;
+
+public class MergeValidationException extends Exception {
+ private static final long serialVersionUID = 1L;
+ private final CommitMergeStatus status;
+
+ public MergeValidationException(CommitMergeStatus status) {
+ super(status.toString());
+ this.status = status;
+ }
+
+ public CommitMergeStatus getStatus() {
+ return status;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java
new file mode 100644
index 0000000..0a8d245
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationListener.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.validators;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.project.ProjectState;
+
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Listener to provide validation of commits before merging.
+ *
+ * Invoked by Gerrit before a commit is merged.
+ */
+@ExtensionPoint
+public interface MergeValidationListener {
+ /**
+ * Validate a commit before it is merged.
+ *
+ * @param repo the repository
+ * @param commit commit details
+ * @param destProject the destination project
+ * @param destBranch the destination branch
+ * @param patchSetId the patch set ID
+ * @throws MergeValidationException if the commit fails to validate
+ */
+ public void onPreMerge(Repository repo,
+ CodeReviewCommit commit,
+ ProjectState destProject,
+ Branch.NameKey destBranch,
+ PatchSet.Id patchSetId)
+ throws MergeValidationException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
new file mode 100644
index 0000000..6eed909
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -0,0 +1,164 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.validators;
+
+import static com.google.gerrit.server.git.MergeUtil.getSubmitter;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.CommitMergeStatus;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Repository;
+
+import java.util.List;
+
+public class MergeValidators {
+ private final DynamicSet<MergeValidationListener> mergeValidationListeners;
+ private final ProjectConfigValidator.Factory projectConfigValidatorFactory;
+
+ public interface Factory {
+ MergeValidators create();
+ }
+
+ @Inject
+ MergeValidators(DynamicSet<MergeValidationListener> mergeValidationListeners,
+ ProjectConfigValidator.Factory projectConfigValidatorFactory) {
+ this.mergeValidationListeners = mergeValidationListeners;
+ this.projectConfigValidatorFactory = projectConfigValidatorFactory;
+ }
+
+ public void validatePreMerge(Repository repo,
+ CodeReviewCommit commit,
+ ProjectState destProject,
+ Branch.NameKey destBranch,
+ PatchSet.Id patchSetId)
+ throws MergeValidationException {
+ List<MergeValidationListener> validators = Lists.newLinkedList();
+
+ validators.add(new PluginMergeValidationListener(mergeValidationListeners));
+ validators.add(projectConfigValidatorFactory.create());
+
+ for (MergeValidationListener validator : validators) {
+ validator.onPreMerge(repo, commit, destProject, destBranch, patchSetId);
+ }
+ }
+
+ public static class ProjectConfigValidator implements
+ MergeValidationListener {
+ private final AllProjectsName allProjectsName;
+ private final ReviewDb db;
+ private final ProjectCache projectCache;
+ private final IdentifiedUser.GenericFactory identifiedUserFactory;
+
+ public interface Factory {
+ ProjectConfigValidator create();
+ }
+
+ @Inject
+ public ProjectConfigValidator(AllProjectsName allProjectsName,
+ ReviewDb db, ProjectCache projectCache,
+ IdentifiedUser.GenericFactory iuf) {
+ this.allProjectsName = allProjectsName;
+ this.db = db;
+ this.projectCache = projectCache;
+ this.identifiedUserFactory = iuf;
+ }
+
+ @Override
+ public void onPreMerge(final Repository repo,
+ final CodeReviewCommit commit,
+ final ProjectState destProject,
+ final Branch.NameKey destBranch,
+ final PatchSet.Id patchSetId)
+ throws MergeValidationException {
+ if (GitRepositoryManager.REF_CONFIG.equals(destBranch.get())) {
+ final Project.NameKey newParent;
+ try {
+ ProjectConfig cfg =
+ new ProjectConfig(destProject.getProject().getNameKey());
+ cfg.load(repo, commit);
+ newParent = cfg.getProject().getParent(allProjectsName);
+ } catch (Exception e) {
+ throw new MergeValidationException(CommitMergeStatus.
+ INVALID_PROJECT_CONFIGURATION);
+ }
+ final Project.NameKey oldParent =
+ destProject.getProject().getParent(allProjectsName);
+ if (oldParent == null) {
+ // update of the 'All-Projects' project
+ if (newParent != null) {
+ throw new MergeValidationException(CommitMergeStatus.
+ INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT);
+ }
+ } else {
+ if (!oldParent.equals(newParent)) {
+ final PatchSetApproval psa = getSubmitter(db, patchSetId);
+ if (psa == null) {
+ throw new MergeValidationException(CommitMergeStatus.
+ SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN);
+ }
+ final IdentifiedUser submitter =
+ identifiedUserFactory.create(psa.getAccountId());
+ if (!submitter.getCapabilities().canAdministrateServer()) {
+ throw new MergeValidationException(CommitMergeStatus.
+ SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN);
+ }
+
+ if (projectCache.get(newParent) == null) {
+ throw new MergeValidationException(CommitMergeStatus.
+ INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /** Execute merge validation plug-ins */
+ public static class PluginMergeValidationListener implements
+ MergeValidationListener {
+ private final DynamicSet<MergeValidationListener> mergeValidationListeners;
+
+ public PluginMergeValidationListener(
+ DynamicSet<MergeValidationListener> mergeValidationListeners) {
+ this.mergeValidationListeners = mergeValidationListeners;
+ }
+
+ @Override
+ public void onPreMerge(Repository repo,
+ CodeReviewCommit commit,
+ ProjectState destProject,
+ Branch.NameKey destBranch,
+ PatchSet.Id patchSetId)
+ throws MergeValidationException {
+ for (MergeValidationListener validator : mergeValidationListeners) {
+ validator.onPreMerge(repo, commit, destProject, destBranch, patchSetId);
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
index aa3f30e..9d0dbf8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
@@ -27,8 +27,8 @@
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupControl;
@@ -43,12 +43,18 @@
import java.util.Map;
public class AddIncludedGroups implements RestModifyView<GroupResource, Input> {
- static class Input {
+ public static class Input {
@DefaultInput
String _oneGroup;
List<String> groups;
+ public static Input fromGroups(List<String> groups) {
+ Input in = new Input();
+ in.groups = groups;
+ return in;
+ }
+
static Input init(Input in) {
if (in == null) {
in = new Input();
@@ -89,8 +95,8 @@
input = Input.init(input);
GroupControl control = resource.getControl();
- Map<AccountGroup.UUID, AccountGroupIncludeByUuid> newIncludedGroups = Maps.newHashMap();
- List<AccountGroupIncludeByUuidAudit> newIncludedGroupsAudits = Lists.newLinkedList();
+ Map<AccountGroup.UUID, AccountGroupById> newIncludedGroups = Maps.newHashMap();
+ List<AccountGroupByIdAud> newIncludedGroupsAudits = Lists.newLinkedList();
List<GroupInfo> result = Lists.newLinkedList();
Account.Id me = ((IdentifiedUser) control.getCurrentUser()).getAccountId();
@@ -102,23 +108,23 @@
}
if (!newIncludedGroups.containsKey(d.getGroupUUID())) {
- AccountGroupIncludeByUuid.Key agiKey =
- new AccountGroupIncludeByUuid.Key(group.getId(),
+ AccountGroupById.Key agiKey =
+ new AccountGroupById.Key(group.getId(),
d.getGroupUUID());
- AccountGroupIncludeByUuid agi = db.accountGroupIncludesByUuid().get(agiKey);
+ AccountGroupById agi = db.accountGroupById().get(agiKey);
if (agi == null) {
- agi = new AccountGroupIncludeByUuid(agiKey);
+ agi = new AccountGroupById(agiKey);
newIncludedGroups.put(d.getGroupUUID(), agi);
- newIncludedGroupsAudits.add(new AccountGroupIncludeByUuidAudit(agi, me));
+ newIncludedGroupsAudits.add(new AccountGroupByIdAud(agi, me));
}
}
result.add(json.format(d));
}
if (!newIncludedGroups.isEmpty()) {
- db.accountGroupIncludesByUuidAudit().insert(newIncludedGroupsAudits);
- db.accountGroupIncludesByUuid().insert(newIncludedGroups.values());
- for (AccountGroupIncludeByUuid agi : newIncludedGroups.values()) {
+ db.accountGroupByIdAud().insert(newIncludedGroupsAudits);
+ db.accountGroupById().insert(newIncludedGroups.values());
+ for (AccountGroupById agi : newIncludedGroups.values()) {
groupIncludeCache.evictMemberIn(agi.getIncludeUUID());
}
groupIncludeCache.evictMembersOf(group.getGroupUUID());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
index 7f32a4d..b3002da41 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -46,12 +46,17 @@
import java.util.List;
import java.util.Map;
-class AddMembers implements RestModifyView<GroupResource, Input> {
- static class Input {
+public class AddMembers implements RestModifyView<GroupResource, Input> {
+ public static class Input {
@DefaultInput
String _oneMember;
List<String> members;
+ public static Input fromMembers(List<String> members) {
+ Input in = new Input();
+ in.members = members;
+ return in;
+ }
static Input init(Input in) {
if (in == null) {
@@ -72,6 +77,7 @@
private final Provider<AccountsCollection> accounts;
private final AccountResolver accountResolver;
private final AccountCache accountCache;
+ private final AccountInfo.Loader.Factory infoFactory;
private final ReviewDb db;
@Inject
@@ -80,12 +86,14 @@
Provider<AccountsCollection> accounts,
AccountResolver accountResolver,
AccountCache accountCache,
+ AccountInfo.Loader.Factory infoFactory,
ReviewDb db) {
this.accountManager = accountManager;
this.authType = authConfig.getAuthType();
this.accounts = accounts;
this.accountResolver = accountResolver;
this.accountCache = accountCache;
+ this.infoFactory = infoFactory;
this.db = db;
}
@@ -104,6 +112,7 @@
List<AccountGroupMemberAudit> newAccountGroupMemberAudits = Lists.newLinkedList();
List<AccountInfo> result = Lists.newLinkedList();
Account.Id me = ((IdentifiedUser) control.getCurrentUser()).getAccountId();
+ AccountInfo.Loader loader = infoFactory.create(true);
for (String nameOrEmail : input.members) {
Account a = findAccount(nameOrEmail);
@@ -126,7 +135,7 @@
newAccountGroupMemberAudits.add(new AccountGroupMemberAudit(m, me));
}
}
- result.add(AccountInfo.parse(a, true));
+ result.add(loader.get(a.getId()));
}
db.accountGroupMembersAudit().insert(newAccountGroupMemberAudits);
@@ -135,6 +144,7 @@
accountCache.evict(m.getAccountId());
}
+ loader.fill();
return result;
}
@@ -213,7 +223,8 @@
}
@Override
- public Object apply(MemberResource resource, PutMember.Input input) {
+ public Object apply(MemberResource resource, PutMember.Input input)
+ throws OrmException {
// Do nothing, the user is already a member.
return get.get().apply(resource);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
index 15f9325..6c98c8f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
@@ -26,8 +26,8 @@
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
@@ -68,8 +68,8 @@
input = Input.init(input);
final GroupControl control = resource.getControl();
- final Map<AccountGroup.UUID, AccountGroupIncludeByUuid> includedGroups = getIncludedGroups(internalGroup.getId());
- final List<AccountGroupIncludeByUuid> toRemove = Lists.newLinkedList();
+ final Map<AccountGroup.UUID, AccountGroupById> includedGroups = getIncludedGroups(internalGroup.getId());
+ final List<AccountGroupById> toRemove = Lists.newLinkedList();
for (final String includedGroup : input.groups) {
GroupDescription.Basic d = groupsCollection.get().parse(includedGroup);
@@ -78,7 +78,7 @@
d.getName()));
}
- AccountGroupIncludeByUuid g = includedGroups.remove(d.getGroupUUID());
+ AccountGroupById g = includedGroups.remove(d.getGroupUUID());
if (g != null) {
toRemove.add(g);
}
@@ -86,8 +86,8 @@
if (!toRemove.isEmpty()) {
writeAudits(toRemove);
- db.accountGroupIncludesByUuid().delete(toRemove);
- for (final AccountGroupIncludeByUuid g : toRemove) {
+ db.accountGroupById().delete(toRemove);
+ for (final AccountGroupById g : toRemove) {
groupIncludeCache.evictMemberIn(g.getIncludeUUID());
}
groupIncludeCache.evictMembersOf(internalGroup.getGroupUUID());
@@ -96,24 +96,24 @@
return Response.none();
}
- private Map<AccountGroup.UUID, AccountGroupIncludeByUuid> getIncludedGroups(
+ private Map<AccountGroup.UUID, AccountGroupById> getIncludedGroups(
final AccountGroup.Id groupId) throws OrmException {
- final Map<AccountGroup.UUID, AccountGroupIncludeByUuid> groups =
+ final Map<AccountGroup.UUID, AccountGroupById> groups =
Maps.newHashMap();
- for (final AccountGroupIncludeByUuid g : db.accountGroupIncludesByUuid().byGroup(groupId)) {
+ for (final AccountGroupById g : db.accountGroupById().byGroup(groupId)) {
groups.put(g.getIncludeUUID(), g);
}
return groups;
}
- private void writeAudits(final List<AccountGroupIncludeByUuid> toBeRemoved)
+ private void writeAudits(final List<AccountGroupById> toBeRemoved)
throws OrmException {
final Account.Id me = ((IdentifiedUser) self.get()).getAccountId();
- final List<AccountGroupIncludeByUuidAudit> auditUpdates = Lists.newLinkedList();
- for (final AccountGroupIncludeByUuid g : toBeRemoved) {
- AccountGroupIncludeByUuidAudit audit = null;
- for (AccountGroupIncludeByUuidAudit a : db
- .accountGroupIncludesByUuidAudit().byGroupInclude(g.getGroupId(),
+ final List<AccountGroupByIdAud> auditUpdates = Lists.newLinkedList();
+ for (final AccountGroupById g : toBeRemoved) {
+ AccountGroupByIdAud audit = null;
+ for (AccountGroupByIdAud a : db
+ .accountGroupByIdAud().byGroupInclude(g.getGroupId(),
g.getIncludeUUID())) {
if (a.isActive()) {
audit = a;
@@ -126,7 +126,7 @@
auditUpdates.add(audit);
}
}
- db.accountGroupIncludesByUuidAudit().update(auditUpdates);
+ db.accountGroupByIdAud().update(auditUpdates);
}
static class DeleteIncludedGroup implements
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
index 5651001..d98dd24 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
@@ -16,10 +16,22 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.account.AccountInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
public class GetMember implements RestReadView<MemberResource> {
+ private final AccountInfo.Loader.Factory infoFactory;
+
+ @Inject
+ GetMember(AccountInfo.Loader.Factory infoFactory) {
+ this.infoFactory = infoFactory;
+ }
+
@Override
- public AccountInfo apply(MemberResource resource) {
- return AccountInfo.parse(resource.getMember().getAccount(), true);
+ public AccountInfo apply(MemberResource rsrc) throws OrmException {
+ AccountInfo.Loader loader = infoFactory.create(true);
+ AccountInfo info = loader.get(rsrc.getMember().getAccountId());
+ loader.fill();
+ return info;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java
index 32513a7..54fc787 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java
@@ -28,7 +28,7 @@
private final GroupControl control;
- GroupResource(GroupControl control) {
+ public GroupResource(GroupControl control) {
this.control = control;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
index 918a530..91da748 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
@@ -30,7 +30,6 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.account.GroupControl;
@@ -68,7 +67,7 @@
final CurrentUser user = self.get();
if (user instanceof AnonymousUser) {
throw new AuthException("Authentication required");
- } else if(!(user instanceof IdentifiedUser)) {
+ } else if(!(user.isIdentifiedUser())) {
throw new ResourceNotFoundException();
}
@@ -81,7 +80,7 @@
final CurrentUser user = self.get();
if (user instanceof AnonymousUser) {
throw new AuthException("Authentication required");
- } else if(!(user instanceof IdentifiedUser)) {
+ } else if(!(user.isIdentifiedUser())) {
throw new ResourceNotFoundException(id);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
index a352cac..2ffff20 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
@@ -25,7 +25,7 @@
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.group.AddIncludedGroups.PutIncludedGroup;
import com.google.gwtorm.server.OrmException;
@@ -78,8 +78,8 @@
private boolean isMember(AccountGroup parent, GroupDescription.Basic member)
throws OrmException {
- return dbProvider.get().accountGroupIncludesByUuid().get(
- new AccountGroupIncludeByUuid.Key(
+ return dbProvider.get().accountGroupById().get(
+ new AccountGroupById.Key(
parent.getId(),
member.getGroupUUID())) != null;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
index 3691e80..f112e34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
@@ -20,7 +20,7 @@
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.group.GroupJson.GroupInfo;
@@ -58,8 +58,8 @@
boolean ownerOfParent = rsrc.getControl().isOwner();
List<GroupInfo> included = Lists.newArrayList();
- for (AccountGroupIncludeByUuid u : dbProvider.get()
- .accountGroupIncludesByUuid()
+ for (AccountGroupById u : dbProvider.get()
+ .accountGroupById()
.byGroup(rsrc.toAccountGroup().getId())) {
try {
GroupControl i = controlFactory.controlFor(u.getIncludeUUID());
@@ -67,7 +67,7 @@
included.add(json.format(i.getGroup()));
}
} catch (NoSuchGroupException notFound) {
- log.warn(String.format("Group %s no longer available, included into ",
+ log.warn(String.format("Group %s no longer available, included into %s",
u.getIncludeUUID(),
rsrc.getGroup().getName()));
continue;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
index d32c632..e150ceb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
@@ -24,7 +24,7 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.server.account.AccountInfo;
import com.google.gerrit.server.account.GroupCache;
@@ -49,7 +49,7 @@
private boolean recursive;
@Inject
- ListMembers(GroupCache groupCache,
+ protected ListMembers(GroupCache groupCache,
GroupDetailFactory.Factory groupDetailFactory,
AccountInfo.Loader.Factory accountLoaderFactory) {
this.groupCache = groupCache;
@@ -63,8 +63,19 @@
if (resource.toAccountGroup() == null) {
throw new MethodNotAllowedException();
}
+
+ return apply(resource.getGroupUUID());
+ }
+
+ public List<AccountInfo> apply(AccountGroup group)
+ throws MethodNotAllowedException, OrmException {
+ return apply(group.getGroupUUID());
+ }
+
+ public List<AccountInfo> apply(AccountGroup.UUID groupId)
+ throws MethodNotAllowedException, OrmException {
final Map<Account.Id, AccountInfo> members =
- getMembers(resource.getGroupUUID(), new HashSet<AccountGroup.UUID>());
+ getMembers(groupId, new HashSet<AccountGroup.UUID>());
final List<AccountInfo> memberInfos = Lists.newArrayList(members.values());
Collections.sort(memberInfos, new Comparator<AccountInfo>() {
@Override
@@ -108,7 +119,7 @@
if (recursive) {
if (groupDetail.includes != null) {
- for (final AccountGroupIncludeByUuid includedGroup : groupDetail.includes) {
+ for (final AccountGroupById includedGroup : groupDetail.includes) {
if (!seenGroups.contains(includedGroup.getIncludeUUID())) {
members.putAll(getMembers(includedGroup.getIncludeUUID(), seenGroups));
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
new file mode 100644
index 0000000..e997c57
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeBatchIndexer.java
@@ -0,0 +1,381 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.server.index;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MultiProgressMonitor;
+import com.google.gerrit.server.git.MultiProgressMonitor.Task;
+import com.google.gerrit.server.patch.PatchListLoader;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
+import org.eclipse.jgit.util.io.NullOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class ChangeBatchIndexer {
+ private static final Logger log =
+ LoggerFactory.getLogger(ChangeBatchIndexer.class);
+
+ public static class Result {
+ private final long elapsedNanos;
+ private final boolean success;
+ private final int done;
+ private final int failed;
+
+ private Result(Stopwatch sw, boolean success, int done, int failed) {
+ this.elapsedNanos = sw.elapsed(TimeUnit.NANOSECONDS);
+ this.success = success;
+ this.done = done;
+ this.failed = failed;
+ }
+
+ public boolean success() {
+ return success;
+ }
+
+ public int doneCount() {
+ return done;
+ }
+
+ public int failedCount() {
+ return failed;
+ }
+
+ public long elapsed(TimeUnit timeUnit) {
+ return timeUnit.convert(elapsedNanos, TimeUnit.NANOSECONDS);
+ }
+ }
+
+ private final SchemaFactory<ReviewDb> schemaFactory;
+ private final GitRepositoryManager repoManager;
+ private final ListeningScheduledExecutorService executor;
+ private final ChangeIndexer.Factory indexerFactory;
+
+ @Inject
+ ChangeBatchIndexer(SchemaFactory<ReviewDb> schemaFactory,
+ GitRepositoryManager repoManager,
+ @IndexExecutor ListeningScheduledExecutorService executor,
+ ChangeIndexer.Factory indexerFactory) {
+ this.schemaFactory = schemaFactory;
+ this.repoManager = repoManager;
+ this.executor = executor;
+ this.indexerFactory = indexerFactory;
+ }
+
+ public Result indexAll(ChangeIndex index, Iterable<Project.NameKey> projects,
+ int numProjects, int numChanges, OutputStream progressOut,
+ OutputStream verboseOut) {
+ if (progressOut == null) {
+ progressOut = NullOutputStream.INSTANCE;
+ }
+ PrintWriter verboseWriter = verboseOut != null ? new PrintWriter(verboseOut)
+ : null;
+
+ Stopwatch sw = new Stopwatch().start();
+ final MultiProgressMonitor mpm =
+ new MultiProgressMonitor(progressOut, "Reindexing changes");
+ final Task projTask = mpm.beginSubTask("projects",
+ numProjects >= 0 ? numProjects : MultiProgressMonitor.UNKNOWN);
+ final Task doneTask = mpm.beginSubTask(null,
+ numChanges >= 0 ? numChanges : MultiProgressMonitor.UNKNOWN);
+ final Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
+
+ final List<ListenableFuture<?>> futures = Lists.newArrayList();
+ final AtomicBoolean ok = new AtomicBoolean(true);
+
+ for (final Project.NameKey project : projects) {
+ final ListenableFuture<?> future = executor.submit(reindexProject(
+ indexerFactory.create(index), project, doneTask, failedTask,
+ verboseWriter));
+ futures.add(future);
+ future.addListener(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ future.get();
+ } catch (InterruptedException e) {
+ fail(project, e);
+ } catch (ExecutionException e) {
+ fail(project, e);
+ } catch (RuntimeException e) {
+ failAndThrow(project, e);
+ } catch (Error e) {
+ failAndThrow(project, e);
+ } finally {
+ projTask.update(1);
+ }
+ }
+
+ private void fail(Project.NameKey project, Throwable t) {
+ log.error("Failed to index project " + project, t);
+ ok.set(false);
+ }
+
+ private void failAndThrow(Project.NameKey project, RuntimeException e) {
+ fail(project, e);
+ throw e;
+ }
+
+ private void failAndThrow(Project.NameKey project, Error e) {
+ fail(project, e);
+ throw e;
+ }
+ }, MoreExecutors.sameThreadExecutor());
+ }
+
+ try {
+ mpm.waitFor(Futures.transform(Futures.successfulAsList(futures),
+ new AsyncFunction<List<?>, Void>() {
+ @Override
+ public ListenableFuture<Void> apply(List<?> input) {
+ mpm.end();
+ return Futures.immediateFuture(null);
+ }
+ }));
+ } catch (ExecutionException e) {
+ log.error("Error in batch indexer", e);
+ ok.set(false);
+ }
+ return new Result(sw, ok.get(), doneTask.getCount(), failedTask.getCount());
+ }
+
+ private Callable<Void> reindexProject(final ChangeIndexer indexer,
+ final Project.NameKey project, final Task done, final Task failed,
+ final PrintWriter verboseWriter) {
+ return new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ Multimap<ObjectId, ChangeData> byId = ArrayListMultimap.create();
+ ReviewDb db = schemaFactory.open();
+ try {
+ Repository repo = repoManager.openRepository(project);
+ try {
+ Map<String, Ref> refs = repo.getAllRefs();
+ for (Change c : db.changes().byProject(project)) {
+ Ref r = refs.get(c.currentPatchSetId().toRefName());
+ if (r != null) {
+ byId.put(r.getObjectId(), new ChangeData(c));
+ }
+ }
+ new ProjectIndexer(indexer, byId, repo, done, failed, verboseWriter)
+ .call();
+ } finally {
+ repo.close();
+ // TODO(dborowitz): Opening all repositories in a live server may be
+ // wasteful; see if we can determine which ones it is safe to close
+ // with RepositoryCache.close(repo).
+ }
+ } finally {
+ db.close();
+ }
+ return null;
+ }
+ };
+ }
+
+ public static class ProjectIndexer implements Callable<Void> {
+ private final ChangeIndexer indexer;
+ private final Multimap<ObjectId, ChangeData> byId;
+ private final ProgressMonitor done;
+ private final ProgressMonitor failed;
+ private final PrintWriter verboseWriter;
+ private final Repository repo;
+ private RevWalk walk;
+
+ public ProjectIndexer(ChangeIndexer indexer,
+ Multimap<ObjectId, ChangeData> changesByCommitId, Repository repo) {
+ this(indexer, changesByCommitId, repo,
+ NullProgressMonitor.INSTANCE, NullProgressMonitor.INSTANCE, null);
+ }
+
+ ProjectIndexer(ChangeIndexer indexer,
+ Multimap<ObjectId, ChangeData> changesByCommitId, Repository repo,
+ ProgressMonitor done, ProgressMonitor failed, PrintWriter verboseWriter) {
+ this.indexer = indexer;
+ this.byId = changesByCommitId;
+ this.repo = repo;
+ this.done = done;
+ this.failed = failed;
+ this.verboseWriter = verboseWriter;
+ }
+
+ @Override
+ public Void call() throws Exception {
+ walk = new RevWalk(repo);
+ try {
+ // Walk only refs first to cover as many changes as we can without having
+ // to mark every single change.
+ for (Ref ref : repo.getRefDatabase().getRefs(Constants.R_HEADS).values()) {
+ RevObject o = walk.parseAny(ref.getObjectId());
+ if (o instanceof RevCommit) {
+ walk.markStart((RevCommit) o);
+ }
+ }
+
+ RevCommit bCommit;
+ while ((bCommit = walk.next()) != null && !byId.isEmpty()) {
+ if (byId.containsKey(bCommit)) {
+ getPathsAndIndex(bCommit);
+ byId.removeAll(bCommit);
+ }
+ }
+
+ for (ObjectId id : byId.keySet()) {
+ getPathsAndIndex(id);
+ }
+ } finally {
+ walk.release();
+ }
+ return null;
+ }
+
+ private void getPathsAndIndex(ObjectId b) throws Exception {
+ List<ChangeData> cds = Lists.newArrayList(byId.get(b));
+ try {
+ RevCommit bCommit = walk.parseCommit(b);
+ RevTree bTree = bCommit.getTree();
+ RevTree aTree = aFor(bCommit, walk);
+ DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
+ try {
+ df.setRepository(repo);
+ if (!cds.isEmpty()) {
+ List<String> paths = (aTree != null)
+ ? getPaths(df.scan(aTree, bTree))
+ : Collections.<String>emptyList();
+ Iterator<ChangeData> cdit = cds.iterator();
+ for (ChangeData cd ; cdit.hasNext(); cdit.remove()) {
+ cd = cdit.next();
+ try {
+ cd.setCurrentFilePaths(paths);
+ indexer.indexTask(cd).call();
+ done.update(1);
+ if (verboseWriter != null) {
+ verboseWriter.println("Reindexed change " + cd.getId());
+ }
+ } catch (Exception e) {
+ fail("Failed to index change " + cd.getId(), true, e);
+ }
+ }
+ }
+ } finally {
+ df.release();
+ }
+ } catch (Exception e) {
+ fail("Failed to index commit " + b.name(), false, e);
+ for (ChangeData cd : cds) {
+ fail("Failed to index change " + cd.getId(), true, null);
+ }
+ }
+ }
+
+ private List<String> getPaths(List<DiffEntry> filenames) {
+ Set<String> paths = Sets.newTreeSet();
+ for (DiffEntry e : filenames) {
+ if (e.getOldPath() != null) {
+ paths.add(e.getOldPath());
+ }
+ if (e.getNewPath() != null) {
+ paths.add(e.getNewPath());
+ }
+ }
+ return ImmutableList.copyOf(paths);
+ }
+
+ private RevTree aFor(RevCommit b, RevWalk walk) throws IOException {
+ switch (b.getParentCount()) {
+ case 0:
+ return walk.parseTree(emptyTree());
+ case 1:
+ RevCommit a = b.getParent(0);
+ walk.parseBody(a);
+ return walk.parseTree(a.getTree());
+ case 2:
+ return PatchListLoader.automerge(repo, walk, b);
+ default:
+ return null;
+ }
+ }
+
+ private ObjectId emptyTree() throws IOException {
+ ObjectInserter oi = repo.newObjectInserter();
+ try {
+ ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {});
+ oi.flush();
+ return id;
+ } finally {
+ oi.release();
+ }
+ }
+
+ private void fail(String error, boolean failed, Exception e) {
+ if (failed) {
+ this.failed.update(1);
+ }
+
+ if (e != null) {
+ log.warn(error, e);
+ } else {
+ log.warn(error);
+ }
+
+ if (verboseWriter != null) {
+ verboseWriter.println(error);
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
new file mode 100644
index 0000000..20b61e7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -0,0 +1,343 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.TrackingId;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeStatusPredicate;
+import com.google.gwtorm.protobuf.CodecFactory;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.gwtorm.server.OrmException;
+import com.google.protobuf.CodedOutputStream;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Fields indexed on change documents.
+ * <p>
+ * Each field corresponds to both a field name supported by
+ * {@link ChangeQueryBuilder} for querying that field, and a method on
+ * {@link ChangeData} used for populating the corresponding document fields in
+ * the secondary index.
+ */
+public class ChangeField {
+ /** Legacy change ID. */
+ public static final FieldDef<ChangeData, Integer> LEGACY_ID =
+ new FieldDef.Single<ChangeData, Integer>("_id",
+ FieldType.INTEGER, true) {
+ @Override
+ public Integer get(ChangeData input, FillArgs args) {
+ return input.getId().get();
+ }
+ };
+
+ /** Newer style Change-Id key. */
+ public static final FieldDef<ChangeData, String> ID =
+ new FieldDef.Single<ChangeData, String>("change_id",
+ FieldType.PREFIX, false) {
+ @Override
+ public String get(ChangeData input, FillArgs args)
+ throws OrmException {
+ return input.change(args.db).getKey().get();
+ }
+ };
+
+ /** Change status string, in the same format as {@code status:}. */
+ public static final FieldDef<ChangeData, String> STATUS =
+ new FieldDef.Single<ChangeData, String>(ChangeQueryBuilder.FIELD_STATUS,
+ FieldType.EXACT, false) {
+ @Override
+ public String get(ChangeData input, FillArgs args)
+ throws OrmException {
+ return ChangeStatusPredicate.VALUES.get(
+ input.change(args.db).getStatus());
+ }
+ };
+
+ /** Project containing the change. */
+ public static final FieldDef<ChangeData, String> PROJECT =
+ new FieldDef.Single<ChangeData, String>(
+ ChangeQueryBuilder.FIELD_PROJECT, FieldType.EXACT, false) {
+ @Override
+ public String get(ChangeData input, FillArgs args)
+ throws OrmException {
+ return input.change(args.db).getProject().get();
+ }
+ };
+
+ /** Reference (aka branch) the change will submit onto. */
+ public static final FieldDef<ChangeData, String> REF =
+ new FieldDef.Single<ChangeData, String>(
+ ChangeQueryBuilder.FIELD_REF, FieldType.EXACT, false) {
+ @Override
+ public String get(ChangeData input, FillArgs args)
+ throws OrmException {
+ return input.change(args.db).getDest().get();
+ }
+ };
+
+ /** Topic, a short annotation on the branch. */
+ public static final FieldDef<ChangeData, String> TOPIC =
+ new FieldDef.Single<ChangeData, String>(
+ ChangeQueryBuilder.FIELD_TOPIC, FieldType.EXACT, false) {
+ @Override
+ public String get(ChangeData input, FillArgs args)
+ throws OrmException {
+ return input.change(args.db).getTopic();
+ }
+ };
+
+ /** Last update time since January 1, 1970. */
+ public static final FieldDef<ChangeData, Timestamp> UPDATED =
+ new FieldDef.Single<ChangeData, Timestamp>(
+ "updated", FieldType.TIMESTAMP, true) {
+ @Override
+ public Timestamp get(ChangeData input, FillArgs args)
+ throws OrmException {
+ return input.change(args.db).getLastUpdatedOn();
+ }
+ };
+
+ /** Sort key field, duplicates {@link #UPDATED}. */
+ @Deprecated
+ public static final FieldDef<ChangeData, Long> SORTKEY =
+ new FieldDef.Single<ChangeData, Long>(
+ "sortkey", FieldType.LONG, true) {
+ @Override
+ public Long get(ChangeData input, FillArgs args)
+ throws OrmException {
+ return ChangeUtil.parseSortKey(input.change(args.db).getSortKey());
+ }
+ };
+
+ /** List of filenames modified in the current patch set. */
+ public static final FieldDef<ChangeData, Iterable<String>> FILE =
+ new FieldDef.Repeatable<ChangeData, String>(
+ ChangeQueryBuilder.FIELD_FILE, FieldType.EXACT, false) {
+ @Override
+ public Iterable<String> get(ChangeData input, FillArgs args)
+ throws OrmException {
+ return input.currentFilePaths(args.db, args.patchListCache);
+ }
+ };
+
+ /** Owner/creator of the change. */
+ public static final FieldDef<ChangeData, Integer> OWNER =
+ new FieldDef.Single<ChangeData, Integer>(
+ ChangeQueryBuilder.FIELD_OWNER, FieldType.INTEGER, false) {
+ @Override
+ public Integer get(ChangeData input, FillArgs args)
+ throws OrmException {
+ return input.change(args.db).getOwner().get();
+ }
+ };
+
+ /** Reviewer(s) associated with the change. */
+ public static final FieldDef<ChangeData, Iterable<Integer>> REVIEWER =
+ new FieldDef.Repeatable<ChangeData, Integer>(
+ ChangeQueryBuilder.FIELD_REVIEWER, FieldType.INTEGER, false) {
+ @Override
+ public Iterable<Integer> get(ChangeData input, FillArgs args)
+ throws OrmException {
+ Set<Integer> r = Sets.newHashSet();
+ for (PatchSetApproval a : input.allApprovals(args.db)) {
+ r.add(a.getAccountId().get());
+ }
+ return r;
+ }
+ };
+
+ /** Commit id of any PatchSet on the change */
+ public static final FieldDef<ChangeData, Iterable<String>> COMMIT =
+ new FieldDef.Repeatable<ChangeData, String>(
+ ChangeQueryBuilder.FIELD_COMMIT, FieldType.PREFIX, false) {
+ @Override
+ public Iterable<String> get(ChangeData input, FillArgs args)
+ throws OrmException {
+ Set<String> revisions = Sets.newHashSet();
+ for (PatchSet ps : input.patches(args.db)) {
+ revisions.add(ps.getRevision().get());
+ }
+ return revisions;
+ }
+ };
+
+ /** Tracking id extracted from a footer. */
+ public static final FieldDef<ChangeData, Iterable<String>> TR =
+ new FieldDef.Repeatable<ChangeData, String>(
+ ChangeQueryBuilder.FIELD_TR, FieldType.EXACT, false) {
+ @Override
+ public Iterable<String> get(ChangeData input, FillArgs args)
+ throws OrmException {
+ Set<String> r = Sets.newHashSet();
+ for (TrackingId id : input.trackingIds(args.db)) {
+ r.add(id.getTrackingId());
+ }
+ return r;
+ }
+ };
+
+ /** List of labels on the current patch set. */
+ public static final FieldDef<ChangeData, Iterable<String>> LABEL =
+ new FieldDef.Repeatable<ChangeData, String>(
+ ChangeQueryBuilder.FIELD_LABEL, FieldType.EXACT, false) {
+ @Override
+ public Iterable<String> get(ChangeData input, FillArgs args)
+ throws OrmException {
+ Set<String> allApprovals = Sets.newHashSet();
+ Set<String> distinctApprovals = Sets.newHashSet();
+ for (PatchSetApproval a : input.currentApprovals(args.db)) {
+ if (a.getValue() != 0) {
+ allApprovals.add(formatLabel(a.getLabel(), a.getValue(),
+ a.getAccountId()));
+ distinctApprovals.add(formatLabel(a.getLabel(), a.getValue()));
+ }
+ }
+ allApprovals.addAll(distinctApprovals);
+ return allApprovals;
+ }
+ };
+
+ /** Set true if the change has a non-zero label score. */
+ public static final FieldDef<ChangeData, String> REVIEWED =
+ new FieldDef.Single<ChangeData, String>(
+ "reviewed", FieldType.EXACT, false) {
+ @Override
+ public String get(ChangeData input, FillArgs args)
+ throws OrmException {
+ for (PatchSetApproval a : input.currentApprovals(args.db)) {
+ if (a.getValue() != 0) {
+ return "1";
+ }
+ }
+ return null;
+ }
+ };
+
+ public static class ChangeProtoField extends FieldDef.Single<ChangeData, byte[]> {
+ public static final ProtobufCodec<Change> CODEC =
+ CodecFactory.encoder(Change.class);
+
+ private ChangeProtoField() {
+ super("_change", FieldType.STORED_ONLY, true);
+ }
+
+ @Override
+ public byte[] get(ChangeData input, FieldDef.FillArgs args)
+ throws OrmException {
+ return CODEC.encodeToByteArray(input.change(args.db));
+ }
+ }
+
+ /** Serialized change object, used for pre-populating results. */
+ public static final ChangeProtoField CHANGE = new ChangeProtoField();
+
+ public static class PatchSetApprovalProtoField
+ extends FieldDef.Repeatable<ChangeData, byte[]> {
+ public static final ProtobufCodec<PatchSetApproval> CODEC =
+ CodecFactory.encoder(PatchSetApproval.class);
+
+ private PatchSetApprovalProtoField() {
+ super("_approval", FieldType.STORED_ONLY, true);
+ }
+
+ @Override
+ public Iterable<byte[]> get(ChangeData input, FillArgs args)
+ throws OrmException {
+ return toProtos(CODEC, input.currentApprovals(args.db));
+ }
+ }
+
+ /**
+ * Serialized approvals for the current patch set, used for pre-populating
+ * results.
+ */
+ public static final PatchSetApprovalProtoField APPROVAL =
+ new PatchSetApprovalProtoField();
+
+ public static String formatLabel(String label, int value) {
+ return formatLabel(label, value, null);
+ }
+
+ public static String formatLabel(String label, int value, Account.Id accountId) {
+ return label.toLowerCase() + (value >= 0 ? "+" : "") + value
+ + (accountId != null ? "," + accountId.get() : "");
+ }
+
+ /** Commit message of the current patch set. */
+ public static final FieldDef<ChangeData, String> COMMIT_MESSAGE =
+ new FieldDef.Single<ChangeData, String>(ChangeQueryBuilder.FIELD_MESSAGE,
+ FieldType.FULL_TEXT, false) {
+ @Override
+ public String get(ChangeData input, FillArgs args) throws OrmException {
+ try {
+ return input.commitMessage(args.repoManager, args.db);
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ }
+ };
+
+ /** Summary or inline comment. */
+ public static final FieldDef<ChangeData, Iterable<String>> COMMENT =
+ new FieldDef.Repeatable<ChangeData, String>(ChangeQueryBuilder.FIELD_COMMENT,
+ FieldType.FULL_TEXT, false) {
+ @Override
+ public Iterable<String> get(ChangeData input, FillArgs args)
+ throws OrmException {
+ Set<String> r = Sets.newHashSet();
+ for (PatchLineComment c : input.comments(args.db)) {
+ r.add(c.getMessage());
+ }
+ for (ChangeMessage m : input.messages(args.db)) {
+ r.add(m.getMessage());
+ }
+ return r;
+ }
+ };
+
+ private static <T> List<byte[]> toProtos(ProtobufCodec<T> codec, Collection<T> objs)
+ throws OrmException {
+ List<byte[]> result = Lists.newArrayListWithCapacity(objs.size());
+ ByteArrayOutputStream out = new ByteArrayOutputStream(256);
+ try {
+ for (T obj : objs) {
+ out.reset();
+ CodedOutputStream cos = CodedOutputStream.newInstance(out);
+ codec.encode(obj, cos);
+ cos.flush();
+ result.add(out.toByteArray());
+ }
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ return result;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
new file mode 100644
index 0000000..b67faa3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
@@ -0,0 +1,150 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+
+import java.io.IOException;
+
+/**
+ * Secondary index implementation for change documents.
+ * <p>
+ * {@link ChangeData} objects are inserted into the index and are queried by
+ * converting special {@link com.google.gerrit.server.query.Predicate} instances
+ * into index-aware predicates that use the index search results as a source.
+ * <p>
+ * Implementations must be thread-safe and should batch inserts/updates where
+ * appropriate.
+ */
+public interface ChangeIndex {
+ /** Instance indicating secondary index is disabled. */
+ public static final ChangeIndex DISABLED = new ChangeIndex() {
+ @Override
+ public Schema<ChangeData> getSchema() {
+ return null;
+ }
+
+ @Override
+ public ListenableFuture<Void> insert(ChangeData cd) throws IOException {
+ return Futures.immediateFuture(null);
+ }
+
+ @Override
+ public ListenableFuture<Void> replace(ChangeData cd) throws IOException {
+ return Futures.immediateFuture(null);
+ }
+
+ @Override
+ public ListenableFuture<Void> delete(ChangeData cd) throws IOException {
+ return Futures.immediateFuture(null);
+ }
+
+ @Override
+ public void deleteAll() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
+ public ChangeDataSource getSource(Predicate<ChangeData> p, int limit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void close() {
+ // Do nothing.
+ }
+
+ @Override
+ public void markReady(boolean ready) {
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ /** @return the schema version used by this index. */
+ public Schema<ChangeData> getSchema();
+
+ /** Close this index. */
+ public void close();
+
+ /**
+ * Insert a change document into the index.
+ * <p>
+ * Results may not be immediately visible to searchers, but should be visible
+ * within a reasonable amount of time.
+ *
+ * @param cd change document
+ *
+ * @throws IOException if the change could not be inserted.
+ */
+ public ListenableFuture<Void> insert(ChangeData cd) throws IOException;
+
+ /**
+ * Update a change document in the index.
+ * <p>
+ * Semantically equivalent to deleting the document and reinserting it with
+ * new field values. Results may not be immediately visible to searchers, but
+ * should be visible within a reasonable amount of time.
+ *
+ * @param cd change document
+ *
+ * @throws IOException
+ */
+ public ListenableFuture<Void> replace(ChangeData cd) throws IOException;
+
+ /**
+ * Delete a change document from the index.
+ *
+ * @param cd change document.
+ *
+ * @throws IOException
+ */
+ public ListenableFuture<Void> delete(ChangeData cd) throws IOException;
+
+ /**
+ * Delete all change documents from the index.
+ *
+ * @throws IOException
+ */
+ public void deleteAll() throws IOException;
+
+ /**
+ * Convert the given operator predicate into a source searching the index and
+ * returning only the documents matching that predicate.
+ *
+ * @param p the predicate to match. Must be a tree containing only AND, OR,
+ * or NOT predicates as internal nodes, and {@link IndexPredicate}s as
+ * leaves.
+ * @param limit maximum number of results to return.
+ * @return a source of documents matching the predicate.
+ *
+ * @throws QueryParseException if the predicate could not be converted to an
+ * indexed data source.
+ */
+ public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
+ throws QueryParseException;
+
+ /**
+ * Mark whether this index is up-to-date and ready to serve reads.
+ *
+ * @param ready whether the index is ready
+ * @throws IOException
+ */
+ public void markReady(boolean ready) throws IOException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
new file mode 100644
index 0000000..0aa3fc8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.query.change.ChangeData;
+
+import java.util.concurrent.Callable;
+
+/**
+ * Helper for (re)indexing a change document.
+ * <p>
+ * Indexing is run in the background, as it may require substantial work to
+ * compute some of the fields and/or update the index.
+ */
+public abstract class ChangeIndexer {
+ public interface Factory {
+ ChangeIndexer create(ChangeIndex index);
+ ChangeIndexer create(IndexCollection indexes);
+ }
+
+ /** Instance indicating secondary index is disabled. */
+ public static final ChangeIndexer DISABLED = new ChangeIndexer(null) {
+ @Override
+ public ListenableFuture<?> index(ChangeData cd) {
+ return Futures.immediateFuture(null);
+ }
+
+ public Callable<Void> indexTask(ChangeData cd) {
+ return new Callable<Void>() {
+ @Override
+ public Void call() {
+ return null;
+ }
+ };
+ }
+ };
+
+ private final ListeningScheduledExecutorService executor;
+
+ protected ChangeIndexer(ListeningScheduledExecutorService executor) {
+ this.executor = executor;
+ }
+
+ /**
+ * Start indexing a change.
+ *
+ * @param change change to index.
+ * @return future for the indexing task.
+ */
+ public ListenableFuture<?> index(Change change) {
+ return index(new ChangeData(change));
+ }
+
+ /**
+ * Start indexing a change.
+ *
+ * @param change change to index.
+ * @param prop propagator to wrap any created runnables in.
+ * @return future for the indexing task.
+ */
+ public ListenableFuture<?> index(ChangeData cd) {
+ return executor.submit(indexTask(cd));
+ }
+
+ public abstract Callable<Void> indexTask(ChangeData cd);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
new file mode 100644
index 0000000..1de5c99
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
@@ -0,0 +1,127 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import com.google.inject.util.Providers;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.Callable;
+
+/**
+ * Helper for (re)indexing a change document.
+ * <p>
+ * Indexing is run in the background, as it may require substantial work to
+ * compute some of the fields and/or update the index.
+ */
+public class ChangeIndexerImpl extends ChangeIndexer {
+ private static final Logger log =
+ LoggerFactory.getLogger(ChangeIndexerImpl.class);
+
+ private final IndexCollection indexes;
+ private final ChangeIndex index;
+ private final SchemaFactory<ReviewDb> schemaFactory;
+ private final ThreadLocalRequestContext context;
+
+ @AssistedInject
+ ChangeIndexerImpl(@IndexExecutor ListeningScheduledExecutorService executor,
+ SchemaFactory<ReviewDb> schemaFactory,
+ ThreadLocalRequestContext context,
+ @Assisted ChangeIndex index) {
+ super(executor);
+ this.schemaFactory = schemaFactory;
+ this.context = context;
+ this.index = index;
+ this.indexes = null;
+ }
+
+ @AssistedInject
+ ChangeIndexerImpl(@IndexExecutor ListeningScheduledExecutorService executor,
+ SchemaFactory<ReviewDb> schemaFactory,
+ ThreadLocalRequestContext context,
+ @Assisted IndexCollection indexes) {
+ super(executor);
+ this.schemaFactory = schemaFactory;
+ this.context = context;
+ this.index = null;
+ this.indexes = indexes;
+ }
+
+ @Override
+ public Callable<Void> indexTask(ChangeData cd) {
+ return new Task(cd);
+ }
+
+ private class Task implements Callable<Void> {
+ private final ChangeData cd;
+
+ private Task(ChangeData cd) {
+ this.cd = cd;
+ }
+
+ @Override
+ public Void call() throws Exception {
+ try {
+ final ReviewDb db = schemaFactory.open();
+ try {
+ context.setContext(new RequestContext() {
+ @Override
+ public Provider<ReviewDb> getReviewDbProvider() {
+ return Providers.of(db);
+ }
+
+ @Override
+ public CurrentUser getCurrentUser() {
+ throw new OutOfScopeException("No user during ChangeIndexer");
+ }
+ });
+ if (indexes != null) {
+ for (ChangeIndex i : indexes.getWriteIndexes()) {
+ i.replace(cd); // TODO(dborowitz): Parallelize these
+ }
+ } else {
+ index.replace(cd);
+ }
+ return null;
+ } finally {
+ context.setContext(null);
+ db.close();
+ }
+ } catch (Exception e) {
+ log.error(String.format(
+ "Failed to index change %d in %s",
+ cd.getId().get(), cd.getChange().getProject().get()), e);
+ throw e;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "index-change-" + cd.getId().get();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
new file mode 100644
index 0000000..1ed6c47
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -0,0 +1,125 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.server.index;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.query.change.ChangeData;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.util.Arrays;
+import java.util.Map;
+
+/** Secondary index schemas for changes. */
+public class ChangeSchemas {
+ @SuppressWarnings({"unchecked", "deprecation"})
+ static final Schema<ChangeData> V1 = release(
+ ChangeField.LEGACY_ID,
+ ChangeField.ID,
+ ChangeField.STATUS,
+ ChangeField.PROJECT,
+ ChangeField.REF,
+ ChangeField.TOPIC,
+ ChangeField.UPDATED,
+ ChangeField.SORTKEY,
+ ChangeField.FILE,
+ ChangeField.OWNER,
+ ChangeField.REVIEWER,
+ ChangeField.COMMIT,
+ ChangeField.TR,
+ ChangeField.LABEL,
+ ChangeField.REVIEWED,
+ ChangeField.COMMIT_MESSAGE,
+ ChangeField.COMMENT);
+
+ @SuppressWarnings({"unchecked", "deprecation"})
+ static final Schema<ChangeData> V2 = release(
+ ChangeField.LEGACY_ID,
+ ChangeField.ID,
+ ChangeField.STATUS,
+ ChangeField.PROJECT,
+ ChangeField.REF,
+ ChangeField.TOPIC,
+ ChangeField.UPDATED,
+ ChangeField.SORTKEY,
+ ChangeField.FILE,
+ ChangeField.OWNER,
+ ChangeField.REVIEWER,
+ ChangeField.COMMIT,
+ ChangeField.TR,
+ ChangeField.LABEL,
+ ChangeField.REVIEWED,
+ ChangeField.COMMIT_MESSAGE,
+ ChangeField.COMMENT,
+ ChangeField.CHANGE,
+ ChangeField.APPROVAL);
+
+ private static Schema<ChangeData> release(FieldDef<ChangeData, ?>... fields) {
+ return new Schema<ChangeData>(true, Arrays.asList(fields));
+ }
+
+ @SuppressWarnings("unused")
+ private static Schema<ChangeData> developer(FieldDef<ChangeData, ?>... fields) {
+ return new Schema<ChangeData>(false, Arrays.asList(fields));
+ }
+
+ public static final ImmutableMap<Integer, Schema<ChangeData>> ALL;
+
+ public static Schema<ChangeData> get(int version) {
+ Schema<ChangeData> schema = ALL.get(version);
+ checkArgument(schema != null, "Unrecognized schema version: %s", version);
+ return schema;
+ }
+
+ public static Schema<ChangeData> getLatest() {
+ return Iterables.getLast(ALL.values());
+ }
+
+ static {
+ Map<Integer, Schema<ChangeData>> all = Maps.newTreeMap();
+ for (Field f : ChangeSchemas.class.getDeclaredFields()) {
+ if (Modifier.isStatic(f.getModifiers())
+ && Modifier.isFinal(f.getModifiers())
+ && Schema.class.isAssignableFrom(f.getType())) {
+ ParameterizedType t = (ParameterizedType) f.getGenericType();
+ if (t.getActualTypeArguments()[0] == ChangeData.class) {
+ try {
+ @SuppressWarnings("unchecked")
+ Schema<ChangeData> schema = (Schema<ChangeData>) f.get(null);
+ checkArgument(f.getName().startsWith("V"));
+ schema.setVersion(Integer.parseInt(f.getName().substring(1)));
+ all.put(schema.getVersion(), schema);
+ } catch (IllegalArgumentException e) {
+ throw new ExceptionInInitializerError(e);
+ } catch (IllegalAccessException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ } else {
+ throw new ExceptionInInitializerError(
+ "non-ChangeData schema: " + f);
+ }
+ }
+ }
+ if (all.isEmpty()) {
+ throw new ExceptionInInitializerError("no ChangeSchemas found");
+ }
+ ALL = ImmutableMap.copyOf(all);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
new file mode 100644
index 0000000..f40f534
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldDef.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/**
+ * Definition of a field stored in the secondary index.
+ *
+ * @param I input type from which documents are created and search results are
+ * returned.
+ * @param T type that should be extracted from the input object when converting
+ * to an index document.
+ */
+public abstract class FieldDef<I, T> {
+ /** Definition of a single (non-repeatable) field. */
+ public static abstract class Single<I, T> extends FieldDef<I, T> {
+ Single(String name, FieldType<T> type, boolean stored) {
+ super(name, type, stored);
+ }
+
+ @Override
+ public final boolean isRepeatable() {
+ return false;
+ }
+ }
+
+ /** Definition of a repeatable field. */
+ public static abstract class Repeatable<I, T>
+ extends FieldDef<I, Iterable<T>> {
+ Repeatable(String name, FieldType<T> type, boolean stored) {
+ super(name, type, stored);
+ }
+
+ @Override
+ public final boolean isRepeatable() {
+ return true;
+ }
+ }
+
+ /** Arguments needed to fill in missing data in the input object. */
+ public static class FillArgs {
+ final Provider<ReviewDb> db;
+ final GitRepositoryManager repoManager;
+ final PatchListCache patchListCache;
+
+ @Inject
+ FillArgs(Provider<ReviewDb> db,
+ GitRepositoryManager repoManager,
+ PatchListCache patchListCache) {
+ this.db = db;
+ this.repoManager = repoManager;
+ this.patchListCache = patchListCache;
+ }
+ }
+
+ private final String name;
+ private final FieldType<?> type;
+ private final boolean stored;
+
+ private FieldDef(String name, FieldType<?> type, boolean stored) {
+ this.name = name;
+ this.type = type;
+ this.stored = stored;
+ }
+
+ /** @return name of the field. */
+ public final String getName() {
+ return name;
+ }
+
+ /**
+ * @return type of the field; for repeatable fields, the inner type, not the
+ * iterable type.
+ */
+ public final FieldType<?> getType() {
+ return type;
+ }
+
+ /** @return whether the field should be stored in the index. */
+ public final boolean isStored() {
+ return stored;
+ }
+
+ /**
+ * Get the field contents from the input object.
+ *
+ * @param input input object.
+ * @param args arbitrary arguments needed to fill in indexable fields of the
+ * input object.
+ * @return the field value(s) to index.
+ *
+ * @throws OrmException
+ */
+ public abstract T get(I input, FillArgs args) throws OrmException;
+
+ /** @return whether the field is repeatable. */
+ public abstract boolean isRepeatable();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
new file mode 100644
index 0000000..a3247b9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/FieldType.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import java.sql.Timestamp;
+
+
+/** Document field types supported by the secondary index system. */
+public class FieldType<T> {
+ /** A single integer-valued field. */
+ public static final FieldType<Integer> INTEGER =
+ new FieldType<Integer>("INTEGER");
+
+ /** A single integer-valued field. */
+ public static final FieldType<Long> LONG =
+ new FieldType<Long>("LONG");
+
+ /** A single date/time-valued field. */
+ public static final FieldType<Timestamp> TIMESTAMP =
+ new FieldType<Timestamp>("TIMESTAMP");
+
+ /** A string field searched using exact-match semantics. */
+ public static final FieldType<String> EXACT =
+ new FieldType<String>("EXACT");
+
+ /** A string field searched using prefix. */
+ public static final FieldType<String> PREFIX =
+ new FieldType<String>("PREFIX");
+
+ /** A string field searched using fuzzy-match semantics. */
+ public static final FieldType<String> FULL_TEXT =
+ new FieldType<String>("FULL_TEXT");
+
+ /** A field that is only stored as raw bytes and cannot be queried. */
+ public static final FieldType<byte[]> STORED_ONLY =
+ new FieldType<byte[]>("STORED_ONLY");
+
+ private final String name;
+
+ private FieldType(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java
new file mode 100644
index 0000000..c574b21
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexCollection.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.server.index;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/** Dynamic pointers to the index versions used for searching and writing. */
+@Singleton
+public class IndexCollection implements LifecycleListener {
+ private final CopyOnWriteArrayList<ChangeIndex> writeIndexes;
+ private final AtomicReference<ChangeIndex> searchIndex;
+
+ @Inject
+ @VisibleForTesting
+ public IndexCollection() {
+ this.writeIndexes = Lists.newCopyOnWriteArrayList();
+ this.searchIndex = new AtomicReference<ChangeIndex>();
+ }
+
+ /**
+ * @return the current search index version, or null if the secondary index is
+ * disabled.
+ */
+ @Nullable
+ public ChangeIndex getSearchIndex() {
+ return searchIndex.get();
+ }
+
+ public void setSearchIndex(ChangeIndex index) {
+ searchIndex.set(index);
+ }
+
+ public Collection<ChangeIndex> getWriteIndexes() {
+ return Collections.unmodifiableCollection(writeIndexes);
+ }
+
+ public synchronized ChangeIndex addWriteIndex(ChangeIndex index) {
+ int version = index.getSchema().getVersion();
+ for (int i = 0; i < writeIndexes.size(); i++) {
+ if (writeIndexes.get(i).getSchema().getVersion() == version) {
+ return writeIndexes.set(i, index);
+ }
+ }
+ writeIndexes.add(index);
+ return null;
+ }
+
+ public synchronized void removeWriteIndex(int version) {
+ int removeIndex = -1;
+ for (int i = 0; i < writeIndexes.size(); i++) {
+ if (writeIndexes.get(i).getSchema().getVersion() == version) {
+ removeIndex = i;
+ break;
+ }
+ }
+ if (removeIndex >= 0) {
+ writeIndexes.remove(removeIndex);
+ }
+ }
+
+ public ChangeIndex getWriteIndex(int version) {
+ for (ChangeIndex i : writeIndexes) {
+ if (i.getSchema().getVersion() == version) {
+ return i;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void start() {
+ }
+
+ @Override
+ public void stop() {
+ ChangeIndex read = searchIndex.get();
+ if (read != null) {
+ read.close();
+ }
+ for (ChangeIndex write : writeIndexes) {
+ if (write != read) {
+ write.close();
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java
new file mode 100644
index 0000000..d794d4e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexExecutor.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Marker on {@link ListeningScheduledExecutorService} used by secondary
+ * indexing threads.
+ */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface IndexExecutor {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
new file mode 100644
index 0000000..20df35b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -0,0 +1,120 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.git.WorkQueue.Executor;
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.inject.AbstractModule;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Module for non-indexer-specific secondary index setup.
+ * <p>
+ * This module should not be used directly except by specific secondary indexer
+ * implementations (e.g. Lucene).
+ */
+public class IndexModule extends LifecycleModule {
+ public enum IndexType {
+ SQL, LUCENE, SOLR;
+ }
+
+ /** Type of secondary index. */
+ public static IndexType getIndexType(Injector injector) {
+ Config cfg = injector.getInstance(
+ Key.get(Config.class, GerritServerConfig.class));
+ return cfg.getEnum("index", null, "type", IndexType.SQL);
+ }
+
+ private final int threads;
+ private final ListeningScheduledExecutorService indexExecutor;
+
+ public IndexModule(int threads) {
+ this.threads = threads;
+ this.indexExecutor = null;
+ }
+
+ public IndexModule(ListeningScheduledExecutorService indexExecutor) {
+ this.threads = -1;
+ this.indexExecutor = indexExecutor;
+ }
+
+ @Override
+ protected void configure() {
+ bind(ChangeQueryRewriter.class).to(IndexRewriteImpl.class);
+ bind(IndexRewriteImpl.BasicRewritesImpl.class);
+ bind(IndexCollection.class);
+ listener().to(IndexCollection.class);
+ install(new FactoryModuleBuilder()
+ .implement(ChangeIndexer.class, ChangeIndexerImpl.class)
+ .build(ChangeIndexer.Factory.class));
+
+ if (indexExecutor != null) {
+ bind(ListeningScheduledExecutorService.class)
+ .annotatedWith(IndexExecutor.class)
+ .toInstance(indexExecutor);
+ } else {
+ install(new IndexExecutorModule(threads));
+ }
+ }
+
+ @Provides
+ ChangeIndexer getChangeIndexer(
+ ChangeIndexer.Factory factory,
+ IndexCollection indexes) {
+ return factory.create(indexes);
+ }
+
+ private static class IndexExecutorModule extends AbstractModule {
+ private final int threads;
+
+ private IndexExecutorModule(int threads) {
+ this.threads = threads;
+ }
+
+ @Override
+ public void configure() {
+ }
+
+ @Provides
+ @Singleton
+ @IndexExecutor
+ ListeningScheduledExecutorService getIndexExecutor(
+ @GerritServerConfig Config config,
+ WorkQueue workQueue) {
+ int threads = this.threads;
+ if (threads <= 0) {
+ threads = config.getInt("index", null, "threads", 0);
+ }
+ Executor executor;
+ if (threads <= 0) {
+ executor = workQueue.getDefaultQueue();
+ } else {
+ executor = workQueue.createQueue(threads, "index");
+ }
+ return MoreExecutors.listeningDecorator(executor);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java
new file mode 100644
index 0000000..d3b9e95
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexPredicate.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.gerrit.server.query.OperatorPredicate;
+
+/** Index-aware predicate that includes a field type annotation. */
+public abstract class IndexPredicate<I> extends OperatorPredicate<I> {
+ private final FieldDef<I, ?> def;
+
+ public IndexPredicate(FieldDef<I, ?> def, String value) {
+ super(def.getName(), value);
+ this.def = def;
+ }
+
+ protected IndexPredicate(FieldDef<I, ?> def, String name, String value) {
+ super(name, value);
+ this.def = def;
+ }
+
+ public FieldDef<I, ?> getField() {
+ return def;
+ }
+
+ public FieldType<?> getType() {
+ return def.getType();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
new file mode 100644
index 0000000..aab6c64
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
@@ -0,0 +1,285 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.NotPredicate;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.QueryRewriter;
+import com.google.gerrit.server.query.change.AndSource;
+import com.google.gerrit.server.query.change.BasicChangeRewrites;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.gerrit.server.query.change.ChangeStatusPredicate;
+import com.google.gerrit.server.query.change.OrSource;
+import com.google.gerrit.server.query.change.SqlRewriterImpl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.BitSet;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+/** Rewriter that pushes boolean logic into the secondary index. */
+public class IndexRewriteImpl implements ChangeQueryRewriter {
+ /** Set of all open change statuses. */
+ public static final Set<Change.Status> OPEN_STATUSES;
+
+ /** Set of all closed change statuses. */
+ public static final Set<Change.Status> CLOSED_STATUSES;
+
+ static {
+ EnumSet<Change.Status> open = EnumSet.noneOf(Change.Status.class);
+ EnumSet<Change.Status> closed = EnumSet.noneOf(Change.Status.class);
+ for (Change.Status s : Change.Status.values()) {
+ if (s.isOpen()) {
+ open.add(s);
+ } else {
+ closed.add(s);
+ }
+ }
+ OPEN_STATUSES = Sets.immutableEnumSet(open);
+ CLOSED_STATUSES = Sets.immutableEnumSet(closed);
+ }
+
+ @VisibleForTesting
+ static final int MAX_LIMIT = 1000;
+
+ /**
+ * Get the set of statuses that changes matching the given predicate may have.
+ *
+ * @param in predicate
+ * @return the maximal set of statuses that any changes matching the input
+ * predicates may have, based on examining boolean and
+ * {@link ChangeStatusPredicate}s.
+ */
+ public static EnumSet<Change.Status> getPossibleStatus(Predicate<ChangeData> in) {
+ EnumSet<Change.Status> s = extractStatus(in);
+ return s != null ? s : EnumSet.allOf(Change.Status.class);
+ }
+
+ private static EnumSet<Change.Status> extractStatus(Predicate<ChangeData> in) {
+ if (in instanceof ChangeStatusPredicate) {
+ return EnumSet.of(((ChangeStatusPredicate) in).getStatus());
+ } else if (in instanceof NotPredicate) {
+ EnumSet<Status> s = extractStatus(in.getChild(0));
+ return s != null ? EnumSet.complementOf(s) : null;
+ } else if (in instanceof OrPredicate) {
+ EnumSet<Change.Status> r = null;
+ int childrenWithStatus = 0;
+ for (int i = 0; i < in.getChildCount(); i++) {
+ EnumSet<Status> c = extractStatus(in.getChild(i));
+ if (c != null) {
+ if (r == null) {
+ r = EnumSet.noneOf(Change.Status.class);
+ }
+ r.addAll(c);
+ childrenWithStatus++;
+ }
+ }
+ if (r != null && childrenWithStatus < in.getChildCount()) {
+ // At least one child supplied a status but another did not.
+ // Assume all statuses for the children that did not feed a
+ // status at this part of the tree. This matches behavior if
+ // the child was used at the root of a query.
+ return EnumSet.allOf(Change.Status.class);
+ }
+ return r;
+ } else if (in instanceof AndPredicate) {
+ EnumSet<Change.Status> r = null;
+ for (int i = 0; i < in.getChildCount(); i++) {
+ EnumSet<Change.Status> c = extractStatus(in.getChild(i));
+ if (c != null) {
+ if (r == null) {
+ r = EnumSet.allOf(Change.Status.class);
+ }
+ r.retainAll(c);
+ }
+ }
+ return r;
+ }
+ return null;
+ }
+
+ private final IndexCollection indexes;
+ private final Provider<ReviewDb> db;
+ private final BasicRewritesImpl basicRewrites;
+ private final SqlRewriterImpl sqlRewriter;
+
+ @Inject
+ IndexRewriteImpl(IndexCollection indexes,
+ Provider<ReviewDb> db,
+ BasicRewritesImpl basicRewrites,
+ SqlRewriterImpl sqlRewriter) {
+ this.indexes = indexes;
+ this.db = db;
+ this.basicRewrites = basicRewrites;
+ this.sqlRewriter = sqlRewriter;
+ }
+
+ @Override
+ public Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
+ throws QueryParseException {
+ ChangeIndex index = indexes.getSearchIndex();
+ if (index == null) {
+ return sqlRewriter.rewrite(in);
+ }
+ in = basicRewrites.rewrite(in);
+ // Add 1 to specified limit to match behavior of QueryProcessor.
+ int limit = ChangeQueryBuilder.hasLimit(in)
+ ? ChangeQueryBuilder.getLimit(in) + 1
+ : MAX_LIMIT;
+
+ Predicate<ChangeData> out = rewriteImpl(in, index, limit);
+ if (in == out || out instanceof IndexPredicate) {
+ return new IndexedChangeQuery(index, out, limit);
+ } else if (out == null /* cannot rewrite */) {
+ return in;
+ } else {
+ return out;
+ }
+ }
+
+ /**
+ * Rewrite a single predicate subtree.
+ *
+ * @param in predicate to rewrite.
+ * @param index index whose schema determines which fields are indexed.
+ * @param limit maximum number of results to return.
+ * @return {@code null} if no part of this subtree can be queried in the
+ * index directly. {@code in} if this subtree and all its children can be
+ * queried directly in the index. Otherwise, a predicate that is
+ * semantically equivalent, with some of its subtrees wrapped to query the
+ * index directly.
+ * @throws QueryParseException if the underlying index implementation does not
+ * support this predicate.
+ */
+ private Predicate<ChangeData> rewriteImpl(Predicate<ChangeData> in,
+ ChangeIndex index, int limit) throws QueryParseException {
+ if (isIndexPredicate(in, index)) {
+ return in;
+ } else if (!isRewritePossible(in)) {
+ return null; // magic to indicate "in" cannot be rewritten
+ }
+
+ int n = in.getChildCount();
+ BitSet isIndexed = new BitSet(n);
+ BitSet notIndexed = new BitSet(n);
+ BitSet rewritten = new BitSet(n);
+ List<Predicate<ChangeData>> newChildren = Lists.newArrayListWithCapacity(n);
+ for (int i = 0; i < n; i++) {
+ Predicate<ChangeData> c = in.getChild(i);
+ Predicate<ChangeData> nc = rewriteImpl(c, index, limit);
+ if (nc == c) {
+ isIndexed.set(i);
+ newChildren.add(c);
+ } else if (nc == null /* cannot rewrite c */) {
+ notIndexed.set(i);
+ newChildren.add(c);
+ } else {
+ rewritten.set(i);
+ newChildren.add(nc);
+ }
+ }
+
+ if (isIndexed.cardinality() == n) {
+ return in; // All children are indexed, leave as-is for parent.
+ } else if (notIndexed.cardinality() == n) {
+ return null; // Can't rewrite any children, so cannot rewrite in.
+ } else if (rewritten.cardinality() == n) {
+ return in.copy(newChildren); // All children were rewritten.
+ }
+ return partitionChildren(in, newChildren, isIndexed, index, limit);
+ }
+
+ private boolean isIndexPredicate(Predicate<ChangeData> in, ChangeIndex index) {
+ if (!(in instanceof IndexPredicate)) {
+ return false;
+ }
+ IndexPredicate<ChangeData> p = (IndexPredicate<ChangeData>) in;
+ return index.getSchema().getFields().containsKey(p.getField().getName());
+ }
+
+ private Predicate<ChangeData> partitionChildren(
+ Predicate<ChangeData> in,
+ List<Predicate<ChangeData>> newChildren,
+ BitSet isIndexed,
+ ChangeIndex index,
+ int limit) throws QueryParseException {
+ if (isIndexed.cardinality() == 1) {
+ int i = isIndexed.nextSetBit(0);
+ newChildren.add(
+ 0, new IndexedChangeQuery(index, newChildren.remove(i), limit));
+ return copy(in, newChildren);
+ }
+
+ // Group all indexed predicates into a wrapped subtree.
+ List<Predicate<ChangeData>> indexed =
+ Lists.newArrayListWithCapacity(isIndexed.cardinality());
+
+ List<Predicate<ChangeData>> all =
+ Lists.newArrayListWithCapacity(
+ newChildren.size() - isIndexed.cardinality() + 1);
+
+ for (int i = 0; i < newChildren.size(); i++) {
+ Predicate<ChangeData> c = newChildren.get(i);
+ if (isIndexed.get(i)) {
+ indexed.add(c);
+ } else {
+ all.add(c);
+ }
+ }
+ all.add(0, new IndexedChangeQuery(index, in.copy(indexed), limit));
+ return copy(in, all);
+ }
+
+ private Predicate<ChangeData> copy(
+ Predicate<ChangeData> in,
+ List<Predicate<ChangeData>> all) {
+ if (in instanceof AndPredicate) {
+ return new AndSource(db, all);
+ } else if (in instanceof OrPredicate) {
+ return new OrSource(all);
+ }
+ return in.copy(all);
+ }
+
+ private static boolean isRewritePossible(Predicate<ChangeData> p) {
+ return p.getChildCount() > 0 && (
+ p instanceof AndPredicate
+ || p instanceof OrPredicate
+ || p instanceof NotPredicate);
+ }
+
+ static class BasicRewritesImpl extends BasicChangeRewrites {
+ private static final QueryRewriter.Definition<ChangeData, BasicRewritesImpl> mydef =
+ new QueryRewriter.Definition<ChangeData, BasicRewritesImpl>(
+ BasicRewritesImpl.class, SqlRewriterImpl.BUILDER);
+ @Inject
+ BasicRewritesImpl(Provider<ReviewDb> db) {
+ super(mydef, db);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
new file mode 100644
index 0000000..ccd6c89
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
@@ -0,0 +1,157 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.common.base.Function;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Wrapper combining an {@link IndexPredicate} together with a
+ * {@link ChangeDataSource} that returns matching results from the index.
+ * <p>
+ * Appropriate to return as the rootmost predicate that can be processed using
+ * the secondary index; such predicates must also implement
+ * {@link ChangeDataSource} to be chosen by the query processor.
+ */
+public class IndexedChangeQuery extends Predicate<ChangeData>
+ implements ChangeDataSource {
+ private final Predicate<ChangeData> pred;
+ private final int limit;
+ private final ChangeDataSource source;
+
+ public IndexedChangeQuery(ChangeIndex index, Predicate<ChangeData> pred, int limit)
+ throws QueryParseException {
+ this.pred = pred;
+ this.limit = limit;
+ this.source = index.getSource(pred, limit);
+ }
+
+ @Override
+ public int getChildCount() {
+ return 1;
+ }
+
+ @Override
+ public Predicate<ChangeData> getChild(int i) {
+ if (i == 0) {
+ return pred;
+ }
+ throw new ArrayIndexOutOfBoundsException(i);
+ }
+
+ @Override
+ public List<Predicate<ChangeData>> getChildren() {
+ return ImmutableList.of(pred);
+ }
+
+ @Override
+ public int getCardinality() {
+ return source.getCardinality();
+ }
+
+ @Override
+ public boolean hasChange() {
+ return source.hasChange();
+ }
+
+ @Override
+ public ResultSet<ChangeData> read() throws OrmException {
+ final ResultSet<ChangeData> rs = source.read();
+ return new ResultSet<ChangeData>() {
+ @Override
+ public Iterator<ChangeData> iterator() {
+ return Iterables.transform(
+ rs,
+ new Function<ChangeData, ChangeData>() {
+ @Override
+ public
+ ChangeData apply(ChangeData input) {
+ input.cacheFromSource(source);
+ return input;
+ }
+ }).iterator();
+ }
+
+ @Override
+ public List<ChangeData> toList() {
+ List<ChangeData> r = rs.toList();
+ for (ChangeData cd : r) {
+ cd.cacheFromSource(source);
+ }
+ return r;
+ }
+
+ @Override
+ public void close() {
+ rs.close();
+ }
+ };
+ }
+
+ @Override
+ public Predicate<ChangeData> copy(
+ Collection<? extends Predicate<ChangeData>> children) {
+ return this;
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.isFromSource(source) || pred.match(cd);
+ }
+
+ @Override
+ public int getCost() {
+ // Index queries are assumed to be cheaper than any other type of query, so
+ // so try to make sure they get picked. Note that pred's cost may be higher
+ // because it doesn't know whether it's being used in an index query or not.
+ return 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return pred.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null || getClass() != other.getClass()) {
+ return false;
+ }
+ IndexedChangeQuery o = (IndexedChangeQuery) other;
+ return pred.equals(o.pred)
+ && limit == o.limit;
+ }
+
+ @Override
+ public String toString() {
+ return Objects.toStringHelper("index")
+ .add("p", source)
+ .add("limit", limit)
+ .toString();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java
new file mode 100644
index 0000000..8c552d8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/NoIndexModule.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.gerrit.server.query.change.SqlRewriterImpl;
+import com.google.inject.AbstractModule;
+
+public class NoIndexModule extends AbstractModule {
+ // TODO(dborowitz): This module should go away when the index becomes
+ // obligatory, as should the interfaces that exist only to support the
+ // non-index case.
+
+ @Override
+ protected void configure() {
+ bind(ChangeIndex.class).toInstance(ChangeIndex.DISABLED);
+ bind(ChangeIndexer.class).toInstance(ChangeIndexer.DISABLED);
+ bind(ChangeQueryRewriter.class).to(SqlRewriterImpl.class);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/RegexPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/RegexPredicate.java
new file mode 100644
index 0000000..198c7b0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/RegexPredicate.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+public abstract class RegexPredicate<I> extends IndexPredicate<I> {
+ protected RegexPredicate(FieldDef<I, ?> def, String value) {
+ super(def, value);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
new file mode 100644
index 0000000..94d1f9c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.server.index;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+
+/** Specific version of a secondary index schema. */
+public class Schema<T> {
+ private final boolean release;
+ private final ImmutableMap<String, FieldDef<T, ?>> fields;
+ private int version;
+
+ protected Schema(boolean release, Iterable<FieldDef<T, ?>> fields) {
+ this(0, release, fields);
+ }
+
+ @VisibleForTesting
+ public Schema(int version, boolean release,
+ Iterable<FieldDef<T, ?>> fields) {
+ this.version = version;
+ this.release = release;
+ ImmutableMap.Builder<String, FieldDef<T, ?>> b = ImmutableMap.builder();
+ for (FieldDef<T, ?> f : fields) {
+ b.put(f.getName(), f);
+ }
+ this.fields = b.build();
+ }
+
+ public final boolean isRelease() {
+ return release;
+ }
+
+ public final int getVersion() {
+ return version;
+ }
+
+ public final ImmutableMap<String, FieldDef<T, ?>> getFields() {
+ return fields;
+ }
+
+ void setVersion(int version) {
+ this.version = version;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
new file mode 100644
index 0000000..62fba12
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import java.sql.Timestamp;
+
+public abstract class TimestampRangePredicate<I> extends IndexPredicate<I> {
+ protected TimestampRangePredicate(FieldDef<I, Timestamp> def,
+ String name, String value) {
+ super(def, name, value);
+ }
+
+ public abstract Timestamp getMinTimestamp();
+ public abstract Timestamp getMaxTimestamp();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index b16ab2a..5b3f59a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -14,22 +14,29 @@
package com.google.gerrit.server.mail;
+import com.google.common.base.Strings;
+import com.google.common.collect.Ordering;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.CommentRange;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.change.PostReview.NotifyHandling;
import com.google.gerrit.server.patch.PatchFile;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
-import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -37,6 +44,9 @@
/** Send comments, after the author of them hit used Publish Comments in the UI. */
public class CommentSender extends ReplyToChangeSender {
+ private static final Logger log = LoggerFactory
+ .getLogger(CommentSender.class);
+
public static interface Factory {
public CommentSender create(NotifyHandling notify, Change change);
}
@@ -62,9 +72,7 @@
paths.add(p.getFileName());
}
}
- String[] names = paths.toArray(new String[paths.size()]);
- Arrays.sort(names);
- changeData.setCurrentFilePaths(names);
+ changeData.setCurrentFilePaths(Ordering.natural().sortedCopy(paths));
}
@Override
@@ -99,8 +107,7 @@
}
public String getInlineComments(int lines) {
- StringBuilder cmts = new StringBuilder();
-
+ StringBuilder cmts = new StringBuilder();
final Repository repo = getRepository();
try {
PatchList patchList = null;
@@ -116,55 +123,37 @@
PatchFile currentFileData = null;
for (final PatchLineComment c : inlineComments) {
final Patch.Key pk = c.getKey().getParentKey();
- final int lineNbr = c.getLine();
- final short side = c.getSide();
if (!pk.equals(currentFileKey)) {
- cmts.append("....................................................\n");
+ String link = makeLink(pk);
+ if (link != null) {
+ cmts.append(link).append('\n');
+ }
if (Patch.COMMIT_MSG.equals(pk.get())) {
- cmts.append("Commit Message\n");
+ cmts.append("Commit Message:\n\n");
} else {
- cmts.append("File ");
- cmts.append(pk.get());
- cmts.append("\n");
+ cmts.append("File ").append(pk.get()).append(":\n\n");
}
currentFileKey = pk;
if (patchList != null) {
try {
currentFileData =
- new PatchFile(repo, patchList, pk.getFileName());
+ new PatchFile(repo, patchList, pk.get());
} catch (IOException e) {
- // Don't quote the line if we can't load it.
+ log.warn(String.format(
+ "Cannot load %s from %s in %s",
+ pk.getFileName(),
+ patchList.getNewId().name(),
+ projectState.getProject().getName()), e);
+ currentFileData = null;
}
- } else {
- currentFileData = null;
}
}
if (currentFileData != null) {
- int maxLines;
- try {
- maxLines = currentFileData.getLineCount(side);
- } catch (Throwable e) {
- maxLines = lineNbr;
- }
-
- final int startLine = Math.max(1, lineNbr - lines + 1);
- final int stopLine = Math.min(maxLines, lineNbr + lines);
-
- for (int line = startLine; line <= lineNbr; ++line) {
- appendFileLine(cmts, currentFileData, side, line);
- }
-
- cmts.append(c.getMessage().trim());
- cmts.append("\n");
-
- for (int line = lineNbr + 1; line < stopLine; ++line) {
- appendFileLine(cmts, currentFileData, side, line);
- }
+ appendComment(cmts, lines, currentFileData, c);
}
-
cmts.append("\n\n");
}
} finally {
@@ -175,6 +164,60 @@
return cmts.toString();
}
+ private void appendComment(StringBuilder out, int contextLines,
+ PatchFile currentFileData, PatchLineComment comment) {
+ short side = comment.getSide();
+ CommentRange range = comment.getRange();
+ if (range != null) {
+ String prefix = String.format("Line %d: ", range.getStartLine());
+ out.append(prefix);
+ for (int n = range.getStartLine(); n <= range.getEndLine(); n++) {
+ out.append(n == range.getStartLine()
+ ? prefix
+ : Strings.padStart(": ", prefix.length(), ' '));
+ try {
+ String s = currentFileData.getLine(side, n);
+ if (n == range.getStartLine() && n == range.getEndLine()) {
+ s = s.substring(
+ Math.min(range.getStartCharacter(), s.length()),
+ Math.min(range.getEndCharacter(), s.length()));
+ } else if (n == range.getStartLine()) {
+ s = s.substring(Math.min(range.getStartCharacter(), s.length()));
+ } else if (n == range.getEndLine()) {
+ s = s.substring(0, Math.min(range.getEndCharacter(), s.length()));
+ }
+ out.append(s);
+ } catch (Throwable e) {
+ // Don't quote the line if we can't safely convert it.
+ }
+ out.append('\n');
+ }
+ appendQuotedParent(out, comment);
+ out.append(comment.getMessage().trim()).append('\n');
+ } else {
+ int lineNbr = comment.getLine();
+ int maxLines;
+ try {
+ maxLines = currentFileData.getLineCount(side);
+ } catch (Throwable e) {
+ maxLines = lineNbr;
+ }
+
+ final int startLine = Math.max(1, lineNbr - contextLines + 1);
+ final int stopLine = Math.min(maxLines, lineNbr + contextLines);
+
+ for (int line = startLine; line <= lineNbr; ++line) {
+ appendFileLine(out, currentFileData, side, line);
+ }
+ appendQuotedParent(out, comment);
+ out.append(comment.getMessage().trim()).append('\n');
+
+ for (int line = lineNbr + 1; line < stopLine; ++line) {
+ appendFileLine(out, currentFileData, side, line);
+ }
+ }
+ }
+
private void appendFileLine(StringBuilder cmts, PatchFile fileData, short side, int line) {
cmts.append("Line " + line);
try {
@@ -187,6 +230,48 @@
cmts.append("\n");
}
+ private void appendQuotedParent(StringBuilder out, PatchLineComment child) {
+ if (child.getParentUuid() != null) {
+ PatchLineComment parent;
+ try {
+ parent = args.db.get().patchComments().get(
+ new PatchLineComment.Key(
+ child.getKey().getParentKey(),
+ child.getParentUuid()));
+ } catch (OrmException e) {
+ parent = null;
+ }
+ if (parent != null) {
+ String msg = parent.getMessage().trim();
+ if (msg.length() > 75) {
+ msg = msg.substring(0, 75);
+ }
+ int lf = msg.indexOf('\n');
+ if (lf > 0) {
+ msg = msg.substring(0, lf);
+ }
+ out.append("> ").append(msg).append('\n');
+ }
+ }
+ }
+
+ // Makes a link back to the given patch set and file.
+ private String makeLink(Patch.Key patch) {
+ String url = getGerritUrl();
+ if (url == null) {
+ return null;
+ }
+
+ PatchSet.Id ps = patch.getParentKey();
+ Change.Id c = ps.getParentKey();
+ return new StringBuilder()
+ .append(url)
+ .append("#/c/").append(c)
+ .append('/').append(ps.get())
+ .append('/').append(KeyUtil.encode(patch.get()))
+ .toString();
+ }
+
private Repository getRepository() {
try {
return args.server.openRepository(projectState.getProject().getNameKey());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index 3e50283..7798028 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -30,7 +30,6 @@
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
import com.google.gerrit.server.ssh.SshAdvertisedAddresses;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -60,7 +59,6 @@
final List<String> sshAddresses;
final ChangeQueryBuilder.Factory queryBuilder;
- final Provider<ChangeQueryRewriter> queryRewriter;
final Provider<ReviewDb> db;
final RuntimeInstance velocityRuntime;
final EmailSettings settings;
@@ -78,7 +76,7 @@
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
AllProjectsName allProjectsName,
ChangeQueryBuilder.Factory queryBuilder,
- Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db,
+ Provider<ReviewDb> db,
RuntimeInstance velocityRuntime,
EmailSettings settings,
@SshAdvertisedAddresses List<String> sshAddresses) {
@@ -98,7 +96,6 @@
this.urlProvider = urlProvider;
this.allProjectsName = allProjectsName;
this.queryBuilder = queryBuilder;
- this.queryRewriter = queryRewriter;
this.db = db;
this.velocityRuntime = velocityRuntime;
this.settings = settings;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index ce50002..dc09a9c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -87,6 +87,7 @@
init();
format();
+ appendText(velocifyFile("Footer.vm"));
if (shouldSendMessage()) {
if (fromId != null) {
final Account fromUser = args.accountCache.get(fromId).getAccount();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
new file mode 100644
index 0000000..3484dd8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/PatchSetNotificationSender.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals;
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndexer;
+import com.google.gerrit.server.mail.MailUtil.MailRecipients;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+public class PatchSetNotificationSender {
+ private static final Logger log =
+ LoggerFactory.getLogger(PatchSetNotificationSender.class);
+
+ private final ReviewDb db;
+ private final GitRepositoryManager repoManager;
+ private final PatchSetInfoFactory patchSetInfoFactory;
+ private final ApprovalsUtil approvalsUtil;
+ private final AccountResolver accountResolver;
+ private final CreateChangeSender.Factory createChangeSenderFactory;
+ private final ReplacePatchSetSender.Factory replacePatchSetFactory;
+
+ @Inject
+ public PatchSetNotificationSender(ReviewDb db,
+ ChangeHooks hooks,
+ GitRepositoryManager repoManager,
+ PatchSetInfoFactory patchSetInfoFactory,
+ ApprovalsUtil approvalsUtil,
+ AccountResolver accountResolver,
+ CreateChangeSender.Factory createChangeSenderFactory,
+ ReplacePatchSetSender.Factory replacePatchSetFactory,
+ ChangeIndexer indexer) {
+ this.db = db;
+ this.repoManager = repoManager;
+ this.patchSetInfoFactory = patchSetInfoFactory;
+ this.approvalsUtil = approvalsUtil;
+ this.accountResolver = accountResolver;
+ this.createChangeSenderFactory = createChangeSenderFactory;
+ this.replacePatchSetFactory = replacePatchSetFactory;
+ }
+
+ public void send(final boolean newChange,
+ final IdentifiedUser currentUser, final Change updatedChange,
+ final PatchSet updatedPatchSet, final LabelTypes labelTypes)
+ throws OrmException, IOException, PatchSetInfoNotAvailableException {
+ final Repository git = repoManager.openRepository(updatedChange.getProject());
+ try {
+ final RevWalk revWalk = new RevWalk(git);
+ final RevCommit commit;
+ try {
+ commit = revWalk.parseCommit(ObjectId.fromString(
+ updatedPatchSet.getRevision().get()));
+ } finally {
+ revWalk.release();
+ }
+ final PatchSetInfo info = patchSetInfoFactory.get(commit, updatedPatchSet.getId());
+ final List<FooterLine> footerLines = commit.getFooterLines();
+ final Account.Id me = currentUser.getAccountId();
+ final MailRecipients recipients =
+ getRecipientsFromFooters(accountResolver, updatedPatchSet, footerLines);
+ recipients.remove(me);
+
+ if (newChange) {
+ approvalsUtil.addReviewers(db, labelTypes,
+ updatedChange, updatedPatchSet, info,
+ recipients.getReviewers(), Collections.<Account.Id> emptySet());
+ try {
+ CreateChangeSender cm = createChangeSenderFactory.create(updatedChange);
+ cm.setFrom(me);
+ cm.setPatchSet(updatedPatchSet, info);
+ cm.addReviewers(recipients.getReviewers());
+ cm.addExtraCC(recipients.getCcOnly());
+ cm.send();
+ } catch (Exception e) {
+ log.error("Cannot send email for new change " + updatedChange.getId(), e);
+ }
+ } else {
+ final List<PatchSetApproval> patchSetApprovals =
+ db.patchSetApprovals().byChange(
+ updatedChange.getId()).toList();
+ final MailRecipients oldRecipients =
+ getRecipientsFromApprovals(patchSetApprovals);
+ approvalsUtil.addReviewers(db, labelTypes, updatedChange,
+ updatedPatchSet, info, recipients.getReviewers(),
+ oldRecipients.getAll());
+ final ChangeMessage msg =
+ new ChangeMessage(new ChangeMessage.Key(updatedChange.getId(),
+ ChangeUtil.messageUUID(db)), me,
+ updatedPatchSet.getCreatedOn(), updatedPatchSet.getId());
+ msg.setMessage("Uploaded patch set " + updatedPatchSet.getPatchSetId() + ".");
+ try {
+ ReplacePatchSetSender cm = replacePatchSetFactory.create(updatedChange);
+ cm.setFrom(me);
+ cm.setPatchSet(updatedPatchSet, info);
+ cm.setChangeMessage(msg);
+ cm.addReviewers(recipients.getReviewers());
+ cm.addExtraCC(recipients.getCcOnly());
+ cm.send();
+ } catch (Exception e) {
+ log.error("Cannot send email for new patch set " + updatedPatchSet.getId(), e);
+ }
+ }
+ } finally {
+ git.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
index 84304b8..757562c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
@@ -24,8 +24,8 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
@@ -88,9 +88,9 @@
add(matching, nc, state.getProject().getNameKey());
} catch (QueryParseException e) {
log.warn(String.format(
- "Project %s has invalid notify %s filter \"%s\": %s",
+ "Project %s has invalid notify %s filter \"%s\"",
state.getProject().getName(), nc.getName(),
- nc.getFilter(), e.getMessage()));
+ nc.getFilter()), e);
}
}
}
@@ -202,14 +202,13 @@
}
if (filter != null) {
- qb.setAllowFile(true);
+ qb.setAllowFileRegex(true);
Predicate<ChangeData> filterPredicate = qb.parse(filter);
if (p == null) {
p = filterPredicate;
} else {
p = Predicate.and(filterPredicate, p);
}
- p = args.queryRewriter.get().rewrite(p);
}
return p == null ? true : p.match(changeData);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java
deleted file mode 100644
index f9a85b9..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.mail;
-
-import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-/** Send notice to reviewers that a change has been rebased. */
-public class RebasedPatchSetSender extends ReplacePatchSetSender {
- public static interface Factory {
- RebasedPatchSetSender create(Change change);
- }
-
- @Inject
- public RebasedPatchSetSender(EmailArguments ea, @Assisted Change c) {
- super(ea, c);
- }
-
- @Override
- protected void formatChange() throws EmailException {
- appendText(velocifyFile("RebasedPatchSet.vm"));
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
index 72df560..9f9a3a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
@@ -151,9 +151,9 @@
setMissingHeader(hdrs, "Importance", importance);
}
if(expiryDays > 0) {
- Date expiry = new Date(System.currentTimeMillis() +
- expiryDays * 24 * 60 * 60 * 1000 );
- setMissingHeader(hdrs, "Expiry-Date",
+ Date expiry = new Date(System.currentTimeMillis() +
+ expiryDays * 24 * 60 * 60 * 1000L );
+ setMissingHeader(hdrs, "Expiry-Date",
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z").format(expiry));
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
index ff9e6cf..852165c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -171,7 +171,10 @@
final List<String> headerLines = new ArrayList<String>(m.size() - 1);
for (int i = 1; i < m.size() - 1; i++) {
final int b = m.get(i);
- final int e = m.get(i + 1);
+ int e = m.get(i + 1);
+ if (header[e - 1] == '\n') {
+ e--;
+ }
headerLines.add(RawParseUtils.decode(Constants.CHARSET, header, b, e));
}
return headerLines;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index bb231a5..e1294e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -64,7 +64,7 @@
import java.util.List;
import java.util.Map;
-class PatchListLoader extends CacheLoader<PatchListKey, PatchList> {
+public class PatchListLoader extends CacheLoader<PatchListKey, PatchList> {
static final Logger log = LoggerFactory.getLogger(PatchListLoader.class);
private final GitRepositoryManager repoManager;
@@ -241,8 +241,13 @@
}
}
- private static RevObject automerge(Repository repo, RevWalk rw, RevCommit b)
+ public static RevTree automerge(Repository repo, RevWalk rw, RevCommit b)
throws IOException {
+ return automerge(repo, rw, b, true);
+ }
+
+ public static RevTree automerge(Repository repo, RevWalk rw, RevCommit b,
+ boolean save) throws IOException {
String hash = b.name();
String refName = GitRepositoryManager.REFS_CACHE_AUTOMERGE
+ hash.substring(0, 2)
@@ -373,10 +378,12 @@
ins.release();
}
- RefUpdate update = repo.updateRef(refName);
- update.setNewObjectId(treeId);
- update.disableRefLog();
- update.forceUpdate();
+ if (save) {
+ RefUpdate update = repo.updateRef(refName);
+ update.setNewObjectId(treeId);
+ update.disableRefLog();
+ update.forceUpdate();
+ }
return rw.parseTree(treeId);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
new file mode 100644
index 0000000..c92f515
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -0,0 +1,519 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch;
+
+import com.google.gerrit.common.data.CommentDetail;
+import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.common.data.PatchScript.DisplayMethod;
+import com.google.gerrit.prettify.common.EditList;
+import com.google.gerrit.prettify.common.SparseFileContent;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.server.FileTypeRegistry;
+import com.google.inject.Inject;
+
+import eu.medsea.mimeutil.MimeType;
+import eu.medsea.mimeutil.MimeUtil2;
+
+import org.eclipse.jgit.diff.Edit;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+class PatchScriptBuilder {
+ static final int MAX_CONTEXT = 5000000;
+ static final int BIG_FILE = 9000;
+
+ private static final Comparator<Edit> EDIT_SORT = new Comparator<Edit>() {
+ @Override
+ public int compare(final Edit o1, final Edit o2) {
+ return o1.getBeginA() - o2.getBeginA();
+ }
+ };
+
+ private Repository db;
+ private Project.NameKey projectKey;
+ private ObjectReader reader;
+ private Change change;
+ private AccountDiffPreference diffPrefs;
+ private boolean againstParent;
+ private ObjectId aId;
+ private ObjectId bId;
+
+ private final Side a;
+ private final Side b;
+
+ private List<Edit> edits;
+ private final FileTypeRegistry registry;
+ private final PatchListCache patchListCache;
+ private int context;
+
+ @Inject
+ PatchScriptBuilder(final FileTypeRegistry ftr, final PatchListCache plc) {
+ a = new Side();
+ b = new Side();
+ registry = ftr;
+ patchListCache = plc;
+ }
+
+ void setRepository(Repository r, Project.NameKey projectKey) {
+ this.db = r;
+ this.projectKey = projectKey;
+ }
+
+ void setChange(final Change c) {
+ this.change = c;
+ }
+
+ void setDiffPrefs(final AccountDiffPreference dp) {
+ diffPrefs = dp;
+
+ context = diffPrefs.getContext();
+ if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+ context = MAX_CONTEXT;
+ } else if (context > MAX_CONTEXT) {
+ context = MAX_CONTEXT;
+ }
+ }
+
+ void setTrees(final boolean ap, final ObjectId a, final ObjectId b) {
+ againstParent = ap;
+ aId = a;
+ bId = b;
+ }
+
+ PatchScript toPatchScript(final PatchListEntry content,
+ final CommentDetail comments, final List<Patch> history)
+ throws IOException {
+ reader = db.newObjectReader();
+ try {
+ return build(content, comments, history);
+ } finally {
+ reader.release();
+ }
+ }
+
+ private PatchScript build(final PatchListEntry content,
+ final CommentDetail comments, final List<Patch> history)
+ throws IOException {
+ boolean intralineDifferenceIsPossible = true;
+ boolean intralineFailure = false;
+ boolean intralineTimeout = false;
+
+ a.path = oldName(content);
+ b.path = newName(content);
+
+ a.resolve(null, aId);
+ b.resolve(a, bId);
+
+ edits = new ArrayList<Edit>(content.getEdits());
+
+ if (!isModify(content)) {
+ intralineDifferenceIsPossible = false;
+ } else if (diffPrefs.isIntralineDifference()) {
+ IntraLineDiff d =
+ patchListCache.getIntraLineDiff(new IntraLineDiffKey(a.id, a.src,
+ b.id, b.src, edits, projectKey, bId, b.path,
+ diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE));
+ if (d != null) {
+ switch (d.getStatus()) {
+ case EDIT_LIST:
+ edits = new ArrayList<Edit>(d.getEdits());
+ break;
+
+ case DISABLED:
+ intralineDifferenceIsPossible = false;
+ break;
+
+ case ERROR:
+ intralineDifferenceIsPossible = false;
+ intralineFailure = true;
+ break;
+
+ case TIMEOUT:
+ intralineDifferenceIsPossible = false;
+ intralineTimeout = true;
+ break;
+ }
+ } else {
+ intralineDifferenceIsPossible = false;
+ intralineFailure = true;
+ }
+ }
+
+ ensureCommentsVisible(comments);
+
+ boolean hugeFile = false;
+ if (a.mode == FileMode.GITLINK || b.mode == FileMode.GITLINK) {
+
+ } else if (a.src == b.src && a.size() <= context
+ && content.getEdits().isEmpty()) {
+ // Odd special case; the files are identical (100% rename or copy)
+ // and the user has asked for context that is larger than the file.
+ // Send them the entire file, with an empty edit after the last line.
+ //
+ for (int i = 0; i < a.size(); i++) {
+ a.addLine(i);
+ }
+ edits = new ArrayList<Edit>(1);
+ edits.add(new Edit(a.size(), a.size()));
+
+ } else {
+ if (BIG_FILE < Math.max(a.size(), b.size())) {
+ // IF the file is really large, we disable things to avoid choking
+ // the browser client.
+ //
+ diffPrefs.setContext((short) Math.min(25, context));
+ diffPrefs.setSyntaxHighlighting(false);
+ context = diffPrefs.getContext();
+ hugeFile = true;
+
+ } else {
+ // In order to expand the skipped common lines or syntax highlight the
+ // file properly we need to give the client the complete file contents.
+ // So force our context temporarily to the complete file size.
+ //
+ context = MAX_CONTEXT;
+ }
+ packContent(diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE);
+ }
+
+ return new PatchScript(change.getKey(), content.getChangeType(),
+ content.getOldName(), content.getNewName(), a.fileMode, b.fileMode,
+ content.getHeaderLines(), diffPrefs, a.dst, b.dst, edits,
+ a.displayMethod, b.displayMethod, a.mimeType.toString(),
+ b.mimeType.toString(), comments, history, hugeFile,
+ intralineDifferenceIsPossible, intralineFailure, intralineTimeout);
+ }
+
+ private static boolean isModify(PatchListEntry content) {
+ switch (content.getChangeType()) {
+ case MODIFIED:
+ case COPIED:
+ case RENAMED:
+ return true;
+
+ case ADDED:
+ case DELETED:
+ default:
+ return false;
+ }
+ }
+
+ private static String oldName(final PatchListEntry entry) {
+ switch (entry.getChangeType()) {
+ case ADDED:
+ return null;
+ case DELETED:
+ case MODIFIED:
+ return entry.getNewName();
+ case COPIED:
+ case RENAMED:
+ default:
+ return entry.getOldName();
+ }
+ }
+
+ private static String newName(final PatchListEntry entry) {
+ switch (entry.getChangeType()) {
+ case DELETED:
+ return null;
+ case ADDED:
+ case MODIFIED:
+ case COPIED:
+ case RENAMED:
+ default:
+ return entry.getNewName();
+ }
+ }
+
+ private void ensureCommentsVisible(final CommentDetail comments) {
+ if (comments.getCommentsA().isEmpty() && comments.getCommentsB().isEmpty()) {
+ // No comments, no additional dummy edits are required.
+ //
+ return;
+ }
+
+ // Construct empty Edit blocks around each location where a comment is.
+ // This will force the later packContent method to include the regions
+ // containing comments, potentially combining those regions together if
+ // they have overlapping contexts. UI renders will also be able to make
+ // correct hunks from this, but because the Edit is empty they will not
+ // style it specially.
+ //
+ final List<Edit> empty = new ArrayList<Edit>();
+ int lastLine;
+
+ lastLine = -1;
+ for (PatchLineComment plc : comments.getCommentsA()) {
+ final int a = plc.getLine();
+ if (lastLine != a) {
+ final int b = mapA2B(a - 1);
+ if (0 <= b) {
+ safeAdd(empty, new Edit(a - 1, b));
+ }
+ lastLine = a;
+ }
+ }
+
+ lastLine = -1;
+ for (PatchLineComment plc : comments.getCommentsB()) {
+ final int b = plc.getLine();
+ if (lastLine != b) {
+ final int a = mapB2A(b - 1);
+ if (0 <= a) {
+ safeAdd(empty, new Edit(a, b - 1));
+ }
+ lastLine = b;
+ }
+ }
+
+ // Sort the final list by the index in A, so packContent can combine
+ // them correctly later.
+ //
+ edits.addAll(empty);
+ Collections.sort(edits, EDIT_SORT);
+ }
+
+ private void safeAdd(final List<Edit> empty, final Edit toAdd) {
+ final int a = toAdd.getBeginA();
+ final int b = toAdd.getBeginB();
+ for (final Edit e : edits) {
+ if (e.getBeginA() <= a && a <= e.getEndA()) {
+ return;
+ }
+ if (e.getBeginB() <= b && b <= e.getEndB()) {
+ return;
+ }
+ }
+ empty.add(toAdd);
+ }
+
+ private int mapA2B(final int a) {
+ if (edits.isEmpty()) {
+ // Magic special case of an unmodified file.
+ //
+ return a;
+ }
+
+ for (int i = 0; i < edits.size(); i++) {
+ final Edit e = edits.get(i);
+ if (a < e.getBeginA()) {
+ if (i == 0) {
+ // Special case of context at start of file.
+ //
+ return a;
+ }
+ return e.getBeginB() - (e.getBeginA() - a);
+ }
+ if (e.getBeginA() <= a && a <= e.getEndA()) {
+ return -1;
+ }
+ }
+
+ final Edit last = edits.get(edits.size() - 1);
+ return last.getEndB() + (a - last.getEndA());
+ }
+
+ private int mapB2A(final int b) {
+ if (edits.isEmpty()) {
+ // Magic special case of an unmodified file.
+ //
+ return b;
+ }
+
+ for (int i = 0; i < edits.size(); i++) {
+ final Edit e = edits.get(i);
+ if (b < e.getBeginB()) {
+ if (i == 0) {
+ // Special case of context at start of file.
+ //
+ return b;
+ }
+ return e.getBeginA() - (e.getBeginB() - b);
+ }
+ if (e.getBeginB() <= b && b <= e.getEndB()) {
+ return -1;
+ }
+ }
+
+ final Edit last = edits.get(edits.size() - 1);
+ return last.getEndA() + (b - last.getEndB());
+ }
+
+ private void packContent(boolean ignoredWhitespace) {
+ EditList list = new EditList(edits, context, a.size(), b.size());
+ for (final EditList.Hunk hunk : list.getHunks()) {
+ while (hunk.next()) {
+ if (hunk.isContextLine()) {
+ final String lineA = a.src.getString(hunk.getCurA());
+ a.dst.addLine(hunk.getCurA(), lineA);
+
+ if (ignoredWhitespace) {
+ // If we ignored whitespace in some form, also get the line
+ // from b when it does not exactly match the line from a.
+ //
+ final String lineB = b.src.getString(hunk.getCurB());
+ if (!lineA.equals(lineB)) {
+ b.dst.addLine(hunk.getCurB(), lineB);
+ }
+ }
+ hunk.incBoth();
+ continue;
+ }
+
+ if (hunk.isDeletedA()) {
+ a.addLine(hunk.getCurA());
+ hunk.incA();
+ }
+
+ if (hunk.isInsertedB()) {
+ b.addLine(hunk.getCurB());
+ hunk.incB();
+ }
+ }
+ }
+ }
+
+ private class Side {
+ String path;
+ ObjectId id;
+ FileMode mode;
+ byte[] srcContent;
+ Text src;
+ MimeType mimeType = MimeUtil2.UNKNOWN_MIME_TYPE;
+ DisplayMethod displayMethod = DisplayMethod.DIFF;
+ PatchScript.FileMode fileMode = PatchScript.FileMode.FILE;
+ final SparseFileContent dst = new SparseFileContent();
+
+ int size() {
+ return src != null ? src.size() : 0;
+ }
+
+ void addLine(int line) {
+ dst.addLine(line, src.getString(line));
+ }
+
+ void resolve(final Side other, final ObjectId within) throws IOException {
+ try {
+ final boolean reuse;
+ if (Patch.COMMIT_MSG.equals(path)) {
+ if (againstParent && (aId == within || within.equals(aId))) {
+ id = ObjectId.zeroId();
+ src = Text.EMPTY;
+ srcContent = Text.NO_BYTES;
+ mode = FileMode.MISSING;
+ displayMethod = DisplayMethod.NONE;
+ } else {
+ id = within;
+ src = Text.forCommit(db, reader, within);
+ srcContent = src.getContent();
+ if (src == Text.EMPTY) {
+ mode = FileMode.MISSING;
+ displayMethod = DisplayMethod.NONE;
+ } else {
+ mode = FileMode.REGULAR_FILE;
+ }
+ }
+ reuse = false;
+
+ } else {
+ final TreeWalk tw = find(within);
+
+ id = tw != null ? tw.getObjectId(0) : ObjectId.zeroId();
+ mode = tw != null ? tw.getFileMode(0) : FileMode.MISSING;
+ reuse = other != null && other.id.equals(id) && other.mode == mode;
+
+ if (reuse) {
+ srcContent = other.srcContent;
+
+ } else if (mode.getObjectType() == Constants.OBJ_BLOB) {
+ srcContent = Text.asByteArray(db.open(id, Constants.OBJ_BLOB));
+
+ } else {
+ srcContent = Text.NO_BYTES;
+ }
+
+ if (reuse) {
+ mimeType = other.mimeType;
+ displayMethod = other.displayMethod;
+ src = other.src;
+
+ } else if (srcContent.length > 0 && FileMode.SYMLINK != mode) {
+ mimeType = registry.getMimeType(path, srcContent);
+ if ("image".equals(mimeType.getMediaType())
+ && registry.isSafeInline(mimeType)) {
+ displayMethod = DisplayMethod.IMG;
+ }
+ }
+ }
+
+ if (mode == FileMode.MISSING) {
+ displayMethod = DisplayMethod.NONE;
+ }
+
+ if (!reuse) {
+ if (srcContent == Text.NO_BYTES) {
+ src = Text.EMPTY;
+ } else {
+ src = new Text(srcContent);
+ }
+ }
+
+ if (srcContent.length > 0 && srcContent[srcContent.length - 1] != '\n') {
+ dst.setMissingNewlineAtEnd(true);
+ }
+ dst.setSize(size());
+ dst.setPath(path);
+
+ if (mode == FileMode.SYMLINK) {
+ fileMode = PatchScript.FileMode.SYMLINK;
+ } else if (mode == FileMode.GITLINK) {
+ fileMode = PatchScript.FileMode.GITLINK;
+ }
+ } catch (IOException err) {
+ throw new IOException("Cannot read " + within.name() + ":" + path, err);
+ }
+ }
+
+ private TreeWalk find(final ObjectId within) throws MissingObjectException,
+ IncorrectObjectTypeException, CorruptObjectException, IOException {
+ if (path == null || within == null) {
+ return null;
+ }
+ final RevWalk rw = new RevWalk(reader);
+ final RevTree tree = rw.parseTree(within);
+ return TreeWalk.forPath(reader, path, tree);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
new file mode 100644
index 0000000..9471d3b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -0,0 +1,335 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch;
+
+import com.google.gerrit.common.data.CommentDetail;
+import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LargeObjectException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import javax.annotation.Nullable;
+
+
+public class PatchScriptFactory implements Callable<PatchScript> {
+ public interface Factory {
+ PatchScriptFactory create(
+ ChangeControl control,
+ String fileName,
+ @Assisted("patchSetA") PatchSet.Id patchSetA,
+ @Assisted("patchSetB") PatchSet.Id patchSetB,
+ AccountDiffPreference diffPrefs);
+ }
+
+ private static final Logger log =
+ LoggerFactory.getLogger(PatchScriptFactory.class);
+
+ private final GitRepositoryManager repoManager;
+ private final Provider<PatchScriptBuilder> builderFactory;
+ private final PatchListCache patchListCache;
+ private final ReviewDb db;
+ private final AccountInfoCacheFactory.Factory aicFactory;
+
+ private final String fileName;
+ @Nullable
+ private final PatchSet.Id psa;
+ private final PatchSet.Id psb;
+ private final AccountDiffPreference diffPrefs;
+
+ private final Change.Id changeId;
+
+ private Change change;
+ private Project.NameKey projectKey;
+ private ChangeControl control;
+ private ObjectId aId;
+ private ObjectId bId;
+ private List<Patch> history;
+ private CommentDetail comments;
+
+ @Inject
+ PatchScriptFactory(final GitRepositoryManager grm,
+ Provider<PatchScriptBuilder> builderFactory,
+ final PatchListCache patchListCache, final ReviewDb db,
+ final AccountInfoCacheFactory.Factory aicFactory,
+ @Assisted ChangeControl control,
+ @Assisted final String fileName,
+ @Assisted("patchSetA") @Nullable final PatchSet.Id patchSetA,
+ @Assisted("patchSetB") final PatchSet.Id patchSetB,
+ @Assisted final AccountDiffPreference diffPrefs) {
+ this.repoManager = grm;
+ this.builderFactory = builderFactory;
+ this.patchListCache = patchListCache;
+ this.db = db;
+ this.control = control;
+ this.aicFactory = aicFactory;
+
+ this.fileName = fileName;
+ this.psa = patchSetA;
+ this.psb = patchSetB;
+ this.diffPrefs = diffPrefs;
+
+ changeId = patchSetB.getParentKey();
+ }
+
+ @Override
+ public PatchScript call() throws OrmException, NoSuchChangeException,
+ LargeObjectException {
+ validatePatchSetId(psa);
+ validatePatchSetId(psb);
+
+ change = control.getChange();
+ projectKey = change.getProject();
+
+ aId = psa != null ? toObjectId(db, psa) : null;
+ bId = toObjectId(db, psb);
+
+ if ((psa != null && !control.isPatchVisible(db.patchSets().get(psa), db)) ||
+ (psb != null && !control.isPatchVisible(db.patchSets().get(psb), db))) {
+ throw new NoSuchChangeException(changeId);
+ }
+
+ final Repository git;
+ try {
+ git = repoManager.openRepository(projectKey);
+ } catch (RepositoryNotFoundException e) {
+ log.error("Repository " + projectKey + " not found", e);
+ throw new NoSuchChangeException(changeId, e);
+ } catch (IOException e) {
+ log.error("Cannot open repository " + projectKey, e);
+ throw new NoSuchChangeException(changeId, e);
+ }
+ try {
+ final PatchList list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
+ final PatchScriptBuilder b = newBuilder(list, git);
+ final PatchListEntry content = list.get(fileName);
+
+ loadCommentsAndHistory(content.getChangeType(), //
+ content.getOldName(), //
+ content.getNewName());
+
+ return b.toPatchScript(content, comments, history);
+ } catch (PatchListNotAvailableException e) {
+ throw new NoSuchChangeException(changeId, e);
+ } catch (IOException e) {
+ log.error("File content unavailable", e);
+ throw new NoSuchChangeException(changeId, e);
+ } catch (org.eclipse.jgit.errors.LargeObjectException err) {
+ throw new LargeObjectException("File content is too large", err);
+ } finally {
+ git.close();
+ }
+ }
+
+ private PatchListKey keyFor(final Whitespace whitespace) {
+ return new PatchListKey(projectKey, aId, bId, whitespace);
+ }
+
+ private PatchList listFor(final PatchListKey key)
+ throws PatchListNotAvailableException {
+ return patchListCache.get(key);
+ }
+
+ private PatchScriptBuilder newBuilder(final PatchList list, Repository git) {
+ final AccountDiffPreference dp = new AccountDiffPreference(diffPrefs);
+ final PatchScriptBuilder b = builderFactory.get();
+ b.setRepository(git, projectKey);
+ b.setChange(change);
+ b.setDiffPrefs(dp);
+ b.setTrees(list.isAgainstParent(), list.getOldId(), list.getNewId());
+ return b;
+ }
+
+ private ObjectId toObjectId(final ReviewDb db, final PatchSet.Id psId)
+ throws OrmException, NoSuchChangeException {
+ if (!changeId.equals(psId.getParentKey())) {
+ throw new NoSuchChangeException(changeId);
+ }
+
+ final PatchSet ps = db.patchSets().get(psId);
+ if (ps == null || ps.getRevision() == null
+ || ps.getRevision().get() == null) {
+ throw new NoSuchChangeException(changeId);
+ }
+
+ try {
+ return ObjectId.fromString(ps.getRevision().get());
+ } catch (IllegalArgumentException e) {
+ log.error("Patch set " + psId + " has invalid revision");
+ throw new NoSuchChangeException(changeId, e);
+ }
+ }
+
+ private void validatePatchSetId(final PatchSet.Id psId)
+ throws NoSuchChangeException {
+ if (psId == null) { // OK, means use base;
+ } else if (changeId.equals(psId.getParentKey())) { // OK, same change;
+ } else {
+ throw new NoSuchChangeException(changeId);
+ }
+ }
+
+ private void loadCommentsAndHistory(final ChangeType changeType,
+ final String oldName, final String newName) throws OrmException {
+ history = new ArrayList<Patch>();
+ comments = new CommentDetail(psa, psb);
+
+ final Map<Patch.Key, Patch> byKey = new HashMap<Patch.Key, Patch>();
+ final AccountInfoCacheFactory aic = aicFactory.create();
+
+ // This seems like a cheap trick. It doesn't properly account for a
+ // file that gets renamed between patch set 1 and patch set 2. We
+ // will wind up packing the wrong Patch object because we didn't do
+ // proper rename detection between the patch sets.
+ //
+ for (final PatchSet ps : db.patchSets().byChange(changeId)) {
+ if (!control.isPatchVisible(ps, db)) {
+ continue;
+ }
+ String name = fileName;
+ if (psa != null) {
+ switch (changeType) {
+ case COPIED:
+ case RENAMED:
+ if (ps.getId().equals(psa)) {
+ name = oldName;
+ }
+ break;
+
+ case MODIFIED:
+ case DELETED:
+ case ADDED:
+ case REWRITE:
+ break;
+ }
+ }
+
+ final Patch p = new Patch(new Patch.Key(ps.getId(), name));
+ history.add(p);
+ byKey.put(p.getKey(), p);
+ }
+
+ switch (changeType) {
+ case ADDED:
+ case MODIFIED:
+ loadPublished(byKey, aic, newName);
+ break;
+
+ case DELETED:
+ loadPublished(byKey, aic, newName);
+ break;
+
+ case COPIED:
+ case RENAMED:
+ if (psa != null) {
+ loadPublished(byKey, aic, oldName);
+ }
+ loadPublished(byKey, aic, newName);
+ break;
+
+ case REWRITE:
+ break;
+ }
+
+ final CurrentUser user = control.getCurrentUser();
+ if (user.isIdentifiedUser()) {
+ final Account.Id me = ((IdentifiedUser) user).getAccountId();
+ switch (changeType) {
+ case ADDED:
+ case MODIFIED:
+ loadDrafts(byKey, aic, me, newName);
+ break;
+
+ case DELETED:
+ loadDrafts(byKey, aic, me, newName);
+ break;
+
+ case COPIED:
+ case RENAMED:
+ if (psa != null) {
+ loadDrafts(byKey, aic, me, oldName);
+ }
+ loadDrafts(byKey, aic, me, newName);
+ break;
+
+ case REWRITE:
+ break;
+ }
+ }
+
+ comments.setAccountInfoCache(aic.create());
+ }
+
+ private void loadPublished(final Map<Patch.Key, Patch> byKey,
+ final AccountInfoCacheFactory aic, final String file) throws OrmException {
+ for (PatchLineComment c : db.patchComments().publishedByChangeFile(changeId, file)) {
+ if (comments.include(c)) {
+ aic.want(c.getAuthor());
+ }
+
+ final Patch.Key pKey = c.getKey().getParentKey();
+ final Patch p = byKey.get(pKey);
+ if (p != null) {
+ p.setCommentCount(p.getCommentCount() + 1);
+ }
+ }
+ }
+
+ private void loadDrafts(final Map<Patch.Key, Patch> byKey,
+ final AccountInfoCacheFactory aic, final Account.Id me, final String file)
+ throws OrmException {
+ for (PatchLineComment c : db.patchComments().draftByChangeFileAuthor(changeId, file, me)) {
+ if (comments.include(c)) {
+ aic.want(me);
+ }
+
+ final Patch.Key pKey = c.getKey().getParentKey();
+ final Patch p = byKey.get(pKey);
+ if (p != null) {
+ p.setDraftCount(p.getDraftCount() + 1);
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java
index e2cac6e..6684bfb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java
@@ -18,7 +18,7 @@
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.DefaultInput;
-import com.google.gerrit.extensions.restapi.PutInput;
+import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -38,7 +38,7 @@
static class Input {
@DefaultInput
String url;
- PutInput raw;
+ RawInput raw;
}
private final PluginLoader loader;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPlugin.java
new file mode 100644
index 0000000..bbb2164
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPlugin.java
@@ -0,0 +1,267 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+import org.eclipse.jgit.internal.storage.file.FileSnapshot;
+
+import java.io.File;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+import javax.annotation.Nullable;
+
+class JarPlugin extends Plugin {
+
+ /** Unique key that changes whenever a plugin reloads. */
+ public static final class CacheKey {
+ private final String name;
+
+ CacheKey(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ int id = System.identityHashCode(this);
+ return String.format("Plugin[%s@%x]", name, id);
+ }
+ }
+
+ private final JarFile jarFile;
+ private final Manifest manifest;
+ private final File dataDir;
+ private final ClassLoader classLoader;
+ private Class<? extends Module> sysModule;
+ private Class<? extends Module> sshModule;
+ private Class<? extends Module> httpModule;
+
+ private Injector sysInjector;
+ private Injector sshInjector;
+ private Injector httpInjector;
+ private LifecycleManager manager;
+ private List<ReloadableRegistrationHandle<?>> reloadableHandles;
+
+ public JarPlugin(String name,
+ PluginUser pluginUser,
+ File srcJar,
+ FileSnapshot snapshot,
+ JarFile jarFile,
+ Manifest manifest,
+ File dataDir,
+ ApiType apiType,
+ ClassLoader classLoader,
+ @Nullable Class<? extends Module> sysModule,
+ @Nullable Class<? extends Module> sshModule,
+ @Nullable Class<? extends Module> httpModule) {
+ super(name, srcJar, pluginUser, snapshot, apiType);
+ this.jarFile = jarFile;
+ this.manifest = manifest;
+ this.dataDir = dataDir;
+ this.classLoader = classLoader;
+ this.sysModule = sysModule;
+ this.sshModule = sshModule;
+ this.httpModule = httpModule;
+ }
+
+ File getSrcJar() {
+ return getSrcFile();
+ }
+
+ @Nullable
+ public String getVersion() {
+ Attributes main = manifest.getMainAttributes();
+ return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+ }
+
+ boolean canReload() {
+ Attributes main = manifest.getMainAttributes();
+ String v = main.getValue("Gerrit-ReloadMode");
+ if (Strings.isNullOrEmpty(v) || "reload".equalsIgnoreCase(v)) {
+ return true;
+ } else if ("restart".equalsIgnoreCase(v)) {
+ return false;
+ } else {
+ PluginLoader.log.warn(String.format(
+ "Plugin %s has invalid Gerrit-ReloadMode %s; assuming restart",
+ getName(), v));
+ return false;
+ }
+ }
+
+ void start(PluginGuiceEnvironment env) throws Exception {
+ RequestContext oldContext = env.enter(this);
+ try {
+ startPlugin(env);
+ } finally {
+ env.exit(oldContext);
+ }
+ }
+
+ private void startPlugin(PluginGuiceEnvironment env) throws Exception {
+ Injector root = newRootInjector(env);
+ manager = new LifecycleManager();
+
+ AutoRegisterModules auto = null;
+ if (sysModule == null && sshModule == null && httpModule == null) {
+ auto = new AutoRegisterModules(getName(), env, jarFile, classLoader);
+ auto.discover();
+ }
+
+ if (sysModule != null) {
+ sysInjector = root.createChildInjector(root.getInstance(sysModule));
+ manager.add(sysInjector);
+ } else if (auto != null && auto.sysModule != null) {
+ sysInjector = root.createChildInjector(auto.sysModule);
+ manager.add(sysInjector);
+ } else {
+ sysInjector = root;
+ }
+
+ if (env.hasSshModule()) {
+ List<Module> modules = Lists.newLinkedList();
+ if (getApiType() == ApiType.PLUGIN) {
+ modules.add(env.getSshModule());
+ }
+ if (sshModule != null) {
+ modules.add(sysInjector.getInstance(sshModule));
+ sshInjector = sysInjector.createChildInjector(modules);
+ manager.add(sshInjector);
+ } else if (auto != null && auto.sshModule != null) {
+ modules.add(auto.sshModule);
+ sshInjector = sysInjector.createChildInjector(modules);
+ manager.add(sshInjector);
+ }
+ }
+
+ if (env.hasHttpModule()) {
+ List<Module> modules = Lists.newLinkedList();
+ if (getApiType() == ApiType.PLUGIN) {
+ modules.add(env.getHttpModule());
+ }
+ if (httpModule != null) {
+ modules.add(sysInjector.getInstance(httpModule));
+ httpInjector = sysInjector.createChildInjector(modules);
+ manager.add(httpInjector);
+ } else if (auto != null && auto.httpModule != null) {
+ modules.add(auto.httpModule);
+ httpInjector = sysInjector.createChildInjector(modules);
+ manager.add(httpInjector);
+ }
+ }
+
+ manager.start();
+ }
+
+ private Injector newRootInjector(final PluginGuiceEnvironment env) {
+ List<Module> modules = Lists.newArrayListWithCapacity(4);
+ if (getApiType() == ApiType.PLUGIN) {
+ modules.add(env.getSysModule());
+ }
+ modules.add(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(PluginUser.class).toInstance(getPluginUser());
+ bind(String.class)
+ .annotatedWith(PluginName.class)
+ .toInstance(getName());
+
+ bind(File.class)
+ .annotatedWith(PluginData.class)
+ .toProvider(new Provider<File>() {
+ private volatile boolean ready;
+
+ @Override
+ public File get() {
+ if (!ready) {
+ synchronized (dataDir) {
+ if (!dataDir.exists() && !dataDir.mkdirs()) {
+ throw new ProvisionException(String.format(
+ "Cannot create %s for plugin %s",
+ dataDir.getAbsolutePath(), getName()));
+ }
+ ready = true;
+ }
+ }
+ return dataDir;
+ }
+ });
+ }
+ });
+ return Guice.createInjector(modules);
+ }
+
+ void stop(PluginGuiceEnvironment env) {
+ if (manager != null) {
+ RequestContext oldContext = env.enter(this);
+ try {
+ manager.stop();
+ } finally {
+ env.exit(oldContext);
+ }
+ manager = null;
+ sysInjector = null;
+ sshInjector = null;
+ httpInjector = null;
+ }
+ }
+
+ public JarFile getJarFile() {
+ return jarFile;
+ }
+
+ public Injector getSysInjector() {
+ return sysInjector;
+ }
+
+ @Nullable
+ public Injector getSshInjector() {
+ return sshInjector;
+ }
+
+ @Nullable
+ public Injector getHttpInjector() {
+ return httpInjector;
+ }
+
+ public void add(RegistrationHandle handle) {
+ if (manager != null) {
+ if (handle instanceof ReloadableRegistrationHandle) {
+ if (reloadableHandles == null) {
+ reloadableHandles = Lists.newArrayList();
+ }
+ reloadableHandles.add((ReloadableRegistrationHandle<?>) handle);
+ }
+ manager.add(handle);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
index 8e7192e..0c69ee72 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -16,19 +16,11 @@
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
-import com.google.gerrit.extensions.annotations.PluginData;
-import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.server.PluginUser;
-import com.google.gerrit.server.util.RequestContext;
-import com.google.inject.AbstractModule;
-import com.google.inject.Guice;
import com.google.inject.Injector;
-import com.google.inject.Module;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
import org.eclipse.jgit.internal.storage.file.FileSnapshot;
@@ -41,7 +33,7 @@
import javax.annotation.Nullable;
-public class Plugin {
+public abstract class Plugin {
public static enum ApiType {
EXTENSION, PLUGIN, JS;
}
@@ -61,13 +53,6 @@
}
}
- static {
- // Guice logs warnings about multiple injectors being created.
- // Silence this in case HTTP plugins are used.
- java.util.logging.Logger.getLogger("com.google.inject.servlet.GuiceFilter")
- .setLevel(java.util.logging.Level.OFF);
- }
-
static ApiType getApiType(Manifest manifest) throws InvalidPluginException {
Attributes main = manifest.getMainAttributes();
String v = main.getValue("Gerrit-ApiType");
@@ -83,238 +68,72 @@
}
}
- private final CacheKey cacheKey;
private final String name;
- private final PluginUser pluginUser;
- private final File srcJar;
- private final FileSnapshot snapshot;
- private final JarFile jarFile;
- private final Manifest manifest;
- private final File dataDir;
+ private final File srcFile;
private final ApiType apiType;
- private final ClassLoader classLoader;
private final boolean disabled;
- private Class<? extends Module> sysModule;
- private Class<? extends Module> sshModule;
- private Class<? extends Module> httpModule;
+ private final CacheKey cacheKey;
+ private final PluginUser pluginUser;
+ private final FileSnapshot snapshot;
- private Injector sysInjector;
- private Injector sshInjector;
- private Injector httpInjector;
- private LifecycleManager manager;
+ protected LifecycleManager manager;
+
private List<ReloadableRegistrationHandle<?>> reloadableHandles;
public Plugin(String name,
+ File srcFile,
PluginUser pluginUser,
- File srcJar,
FileSnapshot snapshot,
- JarFile jarFile,
- Manifest manifest,
- File dataDir,
- ApiType apiType,
- ClassLoader classLoader,
- @Nullable Class<? extends Module> sysModule,
- @Nullable Class<? extends Module> sshModule,
- @Nullable Class<? extends Module> httpModule) {
- this.cacheKey = new CacheKey(name);
- this.pluginUser = pluginUser;
+ ApiType apiType) {
this.name = name;
- this.srcJar = srcJar;
- this.snapshot = snapshot;
- this.jarFile = jarFile;
- this.manifest = manifest;
- this.dataDir = dataDir;
+ this.srcFile = srcFile;
this.apiType = apiType;
- this.classLoader = classLoader;
- this.disabled = srcJar.getName().endsWith(".disabled");
- this.sysModule = sysModule;
- this.sshModule = sshModule;
- this.httpModule = httpModule;
+ this.snapshot = snapshot;
+ this.pluginUser = pluginUser;
+ this.cacheKey = new Plugin.CacheKey(name);
+ this.disabled = srcFile.getName().endsWith(".disabled");
}
- File getSrcJar() {
- return srcJar;
+ File getSrcFile() {
+ return srcFile;
}
PluginUser getPluginUser() {
return pluginUser;
}
- public CacheKey getCacheKey() {
- return cacheKey;
- }
-
public String getName() {
return name;
}
@Nullable
- public String getVersion() {
- Attributes main = manifest.getMainAttributes();
- return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
- }
+ public abstract String getVersion();
public ApiType getApiType() {
return apiType;
}
- boolean canReload() {
- Attributes main = manifest.getMainAttributes();
- String v = main.getValue("Gerrit-ReloadMode");
- if (Strings.isNullOrEmpty(v) || "reload".equalsIgnoreCase(v)) {
- return true;
- } else if ("restart".equalsIgnoreCase(v)) {
- return false;
- } else {
- PluginLoader.log.warn(String.format(
- "Plugin %s has invalid Gerrit-ReloadMode %s; assuming restart",
- name, v));
- return false;
- }
- }
-
- boolean isModified(File jar) {
- return snapshot.lastModified() != jar.lastModified();
+ public Plugin.CacheKey getCacheKey() {
+ return cacheKey;
}
public boolean isDisabled() {
return disabled;
}
- void start(PluginGuiceEnvironment env) throws Exception {
- RequestContext oldContext = env.enter(this);
- try {
- startPlugin(env);
- } finally {
- env.exit(oldContext);
- }
- }
+ abstract void start(PluginGuiceEnvironment env) throws Exception;
- private void startPlugin(PluginGuiceEnvironment env) throws Exception {
- Injector root = newRootInjector(env);
- manager = new LifecycleManager();
+ abstract void stop(PluginGuiceEnvironment env);
- AutoRegisterModules auto = null;
- if (sysModule == null && sshModule == null && httpModule == null) {
- auto = new AutoRegisterModules(name, env, jarFile, classLoader);
- auto.discover();
- }
+ public abstract JarFile getJarFile();
- if (sysModule != null) {
- sysInjector = root.createChildInjector(root.getInstance(sysModule));
- manager.add(sysInjector);
- } else if (auto != null && auto.sysModule != null) {
- sysInjector = root.createChildInjector(auto.sysModule);
- manager.add(sysInjector);
- } else {
- sysInjector = root;
- }
-
- if (env.hasSshModule()) {
- List<Module> modules = Lists.newLinkedList();
- if (apiType == ApiType.PLUGIN) {
- modules.add(env.getSshModule());
- }
- if (sshModule != null) {
- modules.add(sysInjector.getInstance(sshModule));
- sshInjector = sysInjector.createChildInjector(modules);
- manager.add(sshInjector);
- } else if (auto != null && auto.sshModule != null) {
- modules.add(auto.sshModule);
- sshInjector = sysInjector.createChildInjector(modules);
- manager.add(sshInjector);
- }
- }
-
- if (env.hasHttpModule()) {
- List<Module> modules = Lists.newLinkedList();
- if (apiType == ApiType.PLUGIN) {
- modules.add(env.getHttpModule());
- }
- if (httpModule != null) {
- modules.add(sysInjector.getInstance(httpModule));
- httpInjector = sysInjector.createChildInjector(modules);
- manager.add(httpInjector);
- } else if (auto != null && auto.httpModule != null) {
- modules.add(auto.httpModule);
- httpInjector = sysInjector.createChildInjector(modules);
- manager.add(httpInjector);
- }
- }
-
- manager.start();
- }
-
- private Injector newRootInjector(final PluginGuiceEnvironment env) {
- List<Module> modules = Lists.newArrayListWithCapacity(4);
- if (apiType == ApiType.PLUGIN) {
- modules.add(env.getSysModule());
- }
- modules.add(new AbstractModule() {
- @Override
- protected void configure() {
- bind(PluginUser.class).toInstance(pluginUser);
- bind(String.class)
- .annotatedWith(PluginName.class)
- .toInstance(name);
-
- bind(File.class)
- .annotatedWith(PluginData.class)
- .toProvider(new Provider<File>() {
- private volatile boolean ready;
-
- @Override
- public File get() {
- if (!ready) {
- synchronized (dataDir) {
- if (!dataDir.exists() && !dataDir.mkdirs()) {
- throw new ProvisionException(String.format(
- "Cannot create %s for plugin %s",
- dataDir.getAbsolutePath(), name));
- }
- ready = true;
- }
- }
- return dataDir;
- }
- });
- }
- });
- return Guice.createInjector(modules);
- }
-
- void stop(PluginGuiceEnvironment env) {
- if (manager != null) {
- RequestContext oldContext = env.enter(this);
- try {
- manager.stop();
- } finally {
- env.exit(oldContext);
- }
- manager = null;
- sysInjector = null;
- sshInjector = null;
- httpInjector = null;
- }
- }
-
- public JarFile getJarFile() {
- return jarFile;
- }
-
- public Injector getSysInjector() {
- return sysInjector;
- }
+ public abstract Injector getSysInjector();
@Nullable
- public Injector getSshInjector() {
- return sshInjector;
- }
+ public abstract Injector getSshInjector();
@Nullable
- public Injector getHttpInjector() {
- return httpInjector;
- }
+ public abstract Injector getHttpInjector();
public void add(RegistrationHandle handle) {
if (manager != null) {
@@ -339,4 +158,10 @@
public String toString() {
return "Plugin [" + name + "]";
}
+
+ abstract boolean canReload();
+
+ boolean isModified(File jar) {
+ return snapshot.lastModified() != jar.lastModified();
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 035592c..fa2c281 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -166,6 +166,9 @@
public static File storeInTemp(String pluginName, InputStream in,
SitePaths sitePaths) throws IOException {
+ if (!sitePaths.tmp_dir.exists()) {
+ sitePaths.tmp_dir.mkdirs();
+ }
return asTemp(in, tempNameFor(pluginName), ".jar", sitePaths.tmp_dir);
}
@@ -213,7 +216,7 @@
log.info(String.format("Disabling plugin %s", name));
File off = new File(pluginsDir, active.getName() + ".jar.disabled");
- active.getSrcJar().renameTo(off);
+ active.getSrcFile().renameTo(off);
unloadPlugin(active);
try {
@@ -240,7 +243,7 @@
log.info(String.format("Enabling plugin %s", name));
File on = new File(pluginsDir, off.getName() + ".jar");
- off.getSrcJar().renameTo(on);
+ off.getSrcFile().renameTo(on);
disabled.remove(name);
runPlugin(name, on, null);
@@ -303,7 +306,7 @@
String name = active.getName();
try {
log.info(String.format("Reloading plugin %s", name));
- runPlugin(name, active.getSrcJar(), active);
+ runPlugin(name, active.getSrcFile(), active);
} catch (PluginInstallException e) {
log.warn(String.format("Cannot reload plugin %s", name), e.getCause());
throw e;
@@ -469,7 +472,7 @@
Class<? extends Module> sysModule = load(sysName, pluginLoader);
Class<? extends Module> sshModule = load(sshName, pluginLoader);
Class<? extends Module> httpModule = load(httpName, pluginLoader);
- Plugin plugin = new Plugin(name, pluginUserFactory.create(name),
+ Plugin plugin = new JarPlugin(name, pluginUserFactory.create(name),
srcJar, snapshot,
jarFile, manifest,
new File(dataDir, name), type, pluginLoader,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
index 4cdafb3..bd1c3c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
@@ -14,21 +14,17 @@
package com.google.gerrit.server.plugins;
-import static com.google.gerrit.server.plugins.PluginResource.PLUGIN_KIND;
-
-import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.RestApiModule;
import com.google.gerrit.extensions.systemstatus.ServerInformation;
import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.inject.AbstractModule;
-public class PluginModule extends RestApiModule {
+public class PluginModule extends AbstractModule {
@Override
protected void configure() {
bind(ServerInformationImpl.class);
bind(ServerInformation.class).to(ServerInformationImpl.class);
bind(PluginCleanerTask.class);
- bind(PluginsCollection.class);
bind(PluginGuiceEnvironment.class);
bind(PluginLoader.class);
bind(CopyConfigModule.class);
@@ -38,13 +34,5 @@
listener().to(PluginLoader.class);
}
});
-
- DynamicMap.mapOf(binder(), PLUGIN_KIND);
- put(PLUGIN_KIND).to(InstallPlugin.Overwrite.class);
- delete(PLUGIN_KIND).to(DisablePlugin.class);
- get(PLUGIN_KIND, "status").to(GetStatus.class);
- post(PLUGIN_KIND, "disable").to(DisablePlugin.class);
- post(PLUGIN_KIND, "enable").to(EnablePlugin.class);
- post(PLUGIN_KIND, "reload").to(ReloadPlugin.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginRestApiModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
new file mode 100644
index 0000000..6b52a59
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginRestApiModule.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import static com.google.gerrit.server.plugins.PluginResource.PLUGIN_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+
+public class PluginRestApiModule extends RestApiModule {
+ @Override
+ protected void configure() {
+ install(new PluginModule());
+ bind(PluginsCollection.class);
+ DynamicMap.mapOf(binder(), PLUGIN_KIND);
+ put(PLUGIN_KIND).to(InstallPlugin.Overwrite.class);
+ delete(PLUGIN_KIND).to(DisablePlugin.class);
+ get(PLUGIN_KIND, "status").to(GetStatus.class);
+ post(PLUGIN_KIND, "disable").to(DisablePlugin.class);
+ post(PLUGIN_KIND, "enable").to(EnablePlugin.class);
+ post(PLUGIN_KIND, "reload").to(ReloadPlugin.class);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
new file mode 100644
index 0000000..6681d94
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchResource.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.server.project.ListBranches.BranchInfo;
+import com.google.inject.TypeLiteral;
+
+public class BranchResource extends ProjectResource {
+ public static final TypeLiteral<RestView<BranchResource>> BRANCH_KIND =
+ new TypeLiteral<RestView<BranchResource>>() {};
+
+ private final BranchInfo branchInfo;
+
+ public BranchResource(ProjectControl control, BranchInfo branchInfo) {
+ super(control);
+ this.branchInfo = branchInfo;
+ }
+
+ public BranchInfo getBranchInfo() {
+ return branchInfo;
+ }
+
+ public Branch.NameKey getBranchKey() {
+ return new Branch.NameKey(getNameKey(), branchInfo.ref);
+ }
+
+ public String getRef() {
+ return branchInfo.ref;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
new file mode 100644
index 0000000..8ae07de
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BranchesCollection.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.project.ListBranches.BranchInfo;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Constants;
+
+import java.io.IOException;
+import java.util.List;
+
+public class BranchesCollection implements
+ ChildCollection<ProjectResource, BranchResource>,
+ AcceptsCreate<ProjectResource> {
+ private final DynamicMap<RestView<BranchResource>> views;
+ private final Provider<ListBranches> list;
+ private final CreateBranch.Factory createBranchFactory;
+
+ @Inject
+ BranchesCollection(DynamicMap<RestView<BranchResource>> views,
+ Provider<ListBranches> list, CreateBranch.Factory createBranchFactory) {
+ this.views = views;
+ this.list = list;
+ this.createBranchFactory = createBranchFactory;
+ }
+
+ @Override
+ public RestView<ProjectResource> list() {
+ return list.get();
+ }
+
+ @Override
+ public BranchResource parse(ProjectResource parent, IdString id)
+ throws ResourceNotFoundException, IOException {
+ String branchName = id.get();
+ if (!branchName.startsWith(Constants.R_REFS)
+ && !branchName.equals(Constants.HEAD)) {
+ branchName = Constants.R_HEADS + branchName;
+ }
+ List<BranchInfo> branches = list.get().apply(parent);
+ for (BranchInfo b : branches) {
+ if (branchName.equals(b.ref)) {
+ return new BranchResource(parent.getControl(), b);
+ }
+ }
+ throw new ResourceNotFoundException();
+ }
+
+ @Override
+ public DynamicMap<RestView<BranchResource>> views() {
+ return views;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public CreateBranch create(ProjectResource parent, IdString name) {
+ return createBranchFactory.create(name.get());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 6397b96..decf90a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -14,8 +14,11 @@
package com.google.gerrit.server.project;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.RefConfigSection;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.reviewdb.client.Account;
@@ -42,6 +45,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -56,10 +60,12 @@
public static class GenericFactory {
private final ProjectControl.GenericFactory projectControl;
+ private final Provider<ReviewDb> db;
@Inject
- GenericFactory(ProjectControl.GenericFactory p) {
+ GenericFactory(ProjectControl.GenericFactory p, Provider<ReviewDb> d) {
projectControl = p;
+ db = d;
}
public ChangeControl controlFor(Change change, CurrentUser user)
@@ -69,8 +75,34 @@
return projectControl.controlFor(projectKey, user).controlFor(change);
} catch (NoSuchProjectException e) {
throw new NoSuchChangeException(change.getId(), e);
+ } catch (IOException e) {
+ // TODO: propagate this exception
+ throw new NoSuchChangeException(change.getId(), e);
}
}
+
+ public ChangeControl controlFor(Change.Id id, CurrentUser user)
+ throws NoSuchChangeException {
+ final Change change;
+ try {
+ change = db.get().changes().get(id);
+ if (change == null) {
+ throw new NoSuchChangeException(id);
+ }
+ } catch (OrmException e) {
+ throw new NoSuchChangeException(id, e);
+ }
+ return controlFor(change, user);
+ }
+
+ public ChangeControl validateFor(Change.Id id, CurrentUser user)
+ throws NoSuchChangeException, OrmException {
+ ChangeControl c = controlFor(id, user);
+ if (!c.isVisible(db.get())) {
+ throw new NoSuchChangeException(c.getChange().getId());
+ }
+ return c;
+ }
}
public static class Factory {
@@ -213,9 +245,28 @@
&& getRefControl().canUpload(); // as long as you can upload too
}
- /** All available label types for this project. */
+ /** All available label types for this change. */
public LabelTypes getLabelTypes() {
- return getProjectControl().getLabelTypes();
+ String destBranch = getChange().getDest().get();
+ List<LabelType> all = getProjectControl().getLabelTypes().getLabelTypes();
+
+ List<LabelType> r = Lists.newArrayListWithCapacity(all.size());
+ for (LabelType l : all) {
+ List<String> refs = l.getRefPatterns();
+ if (refs == null) {
+ r.add(l);
+ } else {
+ for (String refPattern : refs) {
+ if (RefConfigSection.isValid(refPattern)
+ && match(destBranch, refPattern)) {
+ r.add(l);
+ break;
+ }
+ }
+ }
+ }
+
+ return new LabelTypes(r);
}
/** All value ranges of any allowed label permission. */
@@ -235,7 +286,7 @@
/** Is this user the owner of the change? */
public boolean isOwner() {
- if (getCurrentUser() instanceof IdentifiedUser) {
+ if (getCurrentUser().isIdentifiedUser()) {
final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
return i.getAccountId().equals(change.getOwner());
}
@@ -250,7 +301,7 @@
/** Is this user a reviewer for the change? */
public boolean isReviewer(ReviewDb db, @Nullable ChangeData cd)
throws OrmException {
- if (getCurrentUser() instanceof IdentifiedUser) {
+ if (getCurrentUser().isIdentifiedUser()) {
final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
Iterable<PatchSetApproval> results;
if (cd != null) {
@@ -276,7 +327,7 @@
if (getChange().getStatus().isOpen()) {
// A user can always remove themselves.
//
- if (getCurrentUser() instanceof IdentifiedUser) {
+ if (getCurrentUser().isIdentifiedUser()) {
final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
if (i.getAccountId().equals(reviewer)) {
return true; // can remove self
@@ -374,6 +425,11 @@
return resultsToSubmitRecord(evaluator.getSubmitRule(), results);
}
+ private boolean match(String destBranch, String refPattern) {
+ return RefPatternMatcher.getMatcher(refPattern).match(destBranch,
+ this.getRefControl().getCurrentUser().getUserName());
+ }
+
private List<SubmitRecord> cannotSubmitDraft(ReviewDb db, PatchSet patchSet,
ChangeData cd) {
try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
new file mode 100644
index 0000000..2a386a4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectResource.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class ChildProjectResource extends ProjectResource {
+ public static final TypeLiteral<RestView<ChildProjectResource>> CHILD_PROJECT_KIND =
+ new TypeLiteral<RestView<ChildProjectResource>>() {};
+
+ private final ProjectControl child;
+
+ ChildProjectResource(ProjectResource project, ProjectControl child) {
+ super(project);
+ this.child = child;
+ }
+
+ public ProjectControl getChild() {
+ return child;
+ }
+
+ public boolean isDirectChild() {
+ ProjectState parent =
+ Iterables.getFirst(child.getProjectState().parents(), null);
+ return parent != null
+ && getNameKey().equals(parent.getProject().getNameKey());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
new file mode 100644
index 0000000..27f1648
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+
+public class ChildProjectsCollection implements
+ ChildCollection<ProjectResource, ChildProjectResource> {
+ private final Provider<ListChildProjects> list;
+ private final Provider<ProjectsCollection> projectsCollection;
+ private final DynamicMap<RestView<ChildProjectResource>> views;
+
+ @Inject
+ ChildProjectsCollection(Provider<ListChildProjects> list,
+ Provider<ProjectsCollection> projectsCollection,
+ DynamicMap<RestView<ChildProjectResource>> views) {
+ this.list = list;
+ this.projectsCollection = projectsCollection;
+ this.views = views;
+ }
+
+ @Override
+ public RestView<ProjectResource> list() throws ResourceNotFoundException,
+ AuthException {
+ return list.get();
+ }
+
+ @Override
+ public ChildProjectResource parse(ProjectResource parent, IdString id)
+ throws ResourceNotFoundException, IOException {
+ ProjectResource p =
+ projectsCollection.get().parse(TopLevelResource.INSTANCE, id);
+ for (ProjectState pp : p.getControl().getProjectState().parents()) {
+ if (parent.getNameKey().equals(pp.getProject().getNameKey())) {
+ return new ChildProjectResource(parent, p.getControl());
+ }
+ }
+ throw new ResourceNotFoundException(id);
+ }
+
+ @Override
+ public DynamicMap<RestView<ChildProjectResource>> views() {
+ return views;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
new file mode 100644
index 0000000..bc8d5f0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.git.TransferConfig;
+
+import java.util.Map;
+
+public class ConfigInfo {
+ public final String kind = "gerritcodereview#project_config";
+
+ public String description;
+ public InheritedBooleanInfo useContributorAgreements;
+ public InheritedBooleanInfo useContentMerge;
+ public InheritedBooleanInfo useSignedOffBy;
+ public InheritedBooleanInfo requireChangeId;
+ public MaxObjectSizeLimitInfo maxObjectSizeLimit;
+ public SubmitType submitType;
+ public Project.State state;
+
+ public Map<String, CommentLinkInfo> commentlinks;
+ public ThemeInfo theme;
+
+ public ConfigInfo(ProjectState state, TransferConfig config) {
+ Project p = state.getProject();
+ this.description = Strings.emptyToNull(p.getDescription());
+
+ InheritedBooleanInfo useContributorAgreements =
+ new InheritedBooleanInfo();
+ InheritedBooleanInfo useSignedOffBy = new InheritedBooleanInfo();
+ InheritedBooleanInfo useContentMerge = new InheritedBooleanInfo();
+ InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
+
+ useContributorAgreements.value = state.isUseContributorAgreements();
+ useSignedOffBy.value = state.isUseSignedOffBy();
+ useContentMerge.value = state.isUseContentMerge();
+ requireChangeId.value = state.isRequireChangeID();
+
+ useContributorAgreements.configuredValue =
+ p.getUseContributorAgreements();
+ useSignedOffBy.configuredValue = p.getUseSignedOffBy();
+ useContentMerge.configuredValue = p.getUseContentMerge();
+ requireChangeId.configuredValue = p.getRequireChangeID();
+
+ ProjectState parentState = Iterables.getFirst(state.parents(), null);
+ if (parentState != null) {
+ useContributorAgreements.inheritedValue =
+ parentState.isUseContributorAgreements();
+ useSignedOffBy.inheritedValue = parentState.isUseSignedOffBy();
+ useContentMerge.inheritedValue = parentState.isUseContentMerge();
+ requireChangeId.inheritedValue = parentState.isRequireChangeID();
+ }
+
+ this.useContributorAgreements = useContributorAgreements;
+ this.useSignedOffBy = useSignedOffBy;
+ this.useContentMerge = useContentMerge;
+ this.requireChangeId = requireChangeId;
+
+ MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
+ maxObjectSizeLimit.value =
+ config.getEffectiveMaxObjectSizeLimit(state) == config
+ .getMaxObjectSizeLimit() ? config
+ .getFormattedMaxObjectSizeLimit() : p.getMaxObjectSizeLimit();
+ maxObjectSizeLimit.configuredValue = p.getMaxObjectSizeLimit();
+ maxObjectSizeLimit.inheritedValue =
+ config.getFormattedMaxObjectSizeLimit();
+ this.maxObjectSizeLimit = maxObjectSizeLimit;
+
+ this.submitType = p.getSubmitType();
+ this.state = p.getState() != Project.State.ACTIVE ? p.getState() : null;
+
+ this.commentlinks = Maps.newLinkedHashMap();
+ for (CommentLinkInfo cl : state.getCommentLinks()) {
+ this.commentlinks.put(cl.name, cl);
+ }
+
+ this.theme = state.getTheme();
+ }
+
+ public static class InheritedBooleanInfo {
+ public Boolean value;
+ public InheritableBoolean configuredValue;
+ public Boolean inheritedValue;
+ }
+
+ public static class MaxObjectSizeLimitInfo {
+ public String value;
+ public String configuredValue;
+ public String inheritedValue;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
new file mode 100644
index 0000000..0c29ec8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -0,0 +1,236 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.errors.InvalidRevisionException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.CreateBranch.Input;
+import com.google.gerrit.server.project.ListBranches.BranchInfo;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.ObjectWalk;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+public class CreateBranch implements RestModifyView<ProjectResource, Input> {
+ private static final Logger log = LoggerFactory.getLogger(CreateBranch.class);
+
+ static class Input {
+ String ref;
+
+ @DefaultInput
+ String revision;
+ }
+
+ static interface Factory {
+ CreateBranch create(String ref);
+ }
+
+ private final IdentifiedUser identifiedUser;
+ private final GitRepositoryManager repoManager;
+ private final GitReferenceUpdated referenceUpdated;
+ private final ChangeHooks hooks;
+ private String ref;
+
+ @Inject
+ CreateBranch(IdentifiedUser identifiedUser, GitRepositoryManager repoManager,
+ GitReferenceUpdated referenceUpdated, ChangeHooks hooks,
+ @Assisted String ref) {
+ this.identifiedUser = identifiedUser;
+ this.repoManager = repoManager;
+ this.referenceUpdated = referenceUpdated;
+ this.hooks = hooks;
+ this.ref = ref;
+ }
+
+ @Override
+ public BranchInfo apply(ProjectResource rsrc, Input input)
+ throws BadRequestException, AuthException, ResourceConflictException,
+ IOException {
+ if (input == null) {
+ input = new Input();
+ }
+ if (input.ref != null && !ref.equals(input.ref)) {
+ throw new BadRequestException("ref must match URL");
+ }
+ if (input.revision == null) {
+ input.revision = Constants.HEAD;
+ }
+ while (ref.startsWith("/")) {
+ ref = ref.substring(1);
+ }
+ if (!ref.startsWith(Constants.R_REFS)) {
+ ref = Constants.R_HEADS + ref;
+ }
+ if (!Repository.isValidRefName(ref)) {
+ throw new BadRequestException("invalid branch name \"" + ref + "\"");
+ }
+ if (MagicBranch.isMagicBranch(ref)) {
+ throw new BadRequestException("not allowed to create branches under \""
+ + MagicBranch.getMagicRefNamePrefix(ref) + "\"");
+ }
+
+ final Branch.NameKey name = new Branch.NameKey(rsrc.getNameKey(), ref);
+ final RefControl refControl = rsrc.getControl().controlForRef(name);
+ final Repository repo = repoManager.openRepository(rsrc.getNameKey());
+ try {
+ final ObjectId revid = parseBaseRevision(repo, rsrc.getNameKey(), input.revision);
+ final RevWalk rw = verifyConnected(repo, revid);
+ RevObject object = rw.parseAny(revid);
+
+ if (ref.startsWith(Constants.R_HEADS)) {
+ // Ensure that what we start the branch from is a commit. If we
+ // were given a tag, deference to the commit instead.
+ //
+ try {
+ object = rw.parseCommit(object);
+ } catch (IncorrectObjectTypeException notCommit) {
+ throw new BadRequestException("\"" + input.revision + "\" not a commit");
+ }
+ }
+
+ if (!refControl.canCreate(rw, object)) {
+ throw new AuthException("Cannot create \"" + ref + "\"");
+ }
+
+ try {
+ final RefUpdate u = repo.updateRef(ref);
+ u.setExpectedOldObjectId(ObjectId.zeroId());
+ u.setNewObjectId(object.copy());
+ u.setRefLogIdent(identifiedUser.newRefLogIdent());
+ u.setRefLogMessage("created via REST from " + input.revision, false);
+ final RefUpdate.Result result = u.update(rw);
+ switch (result) {
+ case FAST_FORWARD:
+ case NEW:
+ case NO_CHANGE:
+ referenceUpdated.fire(name.getParentKey(), u);
+ hooks.doRefUpdatedHook(name, u, identifiedUser.getAccount());
+ break;
+ case LOCK_FAILURE:
+ if (repo.getRef(ref) != null) {
+ throw new ResourceConflictException("branch \"" + ref
+ + "\" already exists");
+ }
+ String refPrefix = getRefPrefix(ref);
+ while (!Constants.R_HEADS.equals(refPrefix)) {
+ if (repo.getRef(refPrefix) != null) {
+ throw new ResourceConflictException("Cannot create branch \""
+ + ref + "\" since it conflicts with branch \"" + refPrefix
+ + "\".");
+ }
+ refPrefix = getRefPrefix(refPrefix);
+ }
+ default: {
+ throw new IOException(result.name());
+ }
+ }
+
+ BranchInfo b = new BranchInfo();
+ b.ref = ref;
+ b.revision = revid.getName();
+ b.setCanDelete(refControl.canDelete());
+ return b;
+ } catch (IOException err) {
+ log.error("Cannot create branch \"" + name + "\"", err);
+ throw err;
+ }
+ } catch (InvalidRevisionException e) {
+ throw new BadRequestException("invalid revision \"" + input.revision + "\"");
+ } finally {
+ repo.close();
+ }
+ }
+
+ private static String getRefPrefix(final String refName) {
+ final int i = refName.lastIndexOf('/');
+ if (i > Constants.R_HEADS.length() - 1) {
+ return refName.substring(0, i);
+ }
+ return Constants.R_HEADS;
+ }
+
+ private ObjectId parseBaseRevision(Repository repo,
+ Project.NameKey projectName, String baseRevision)
+ throws InvalidRevisionException {
+ try {
+ final ObjectId revid = repo.resolve(baseRevision);
+ if (revid == null) {
+ throw new InvalidRevisionException();
+ }
+ return revid;
+ } catch (IOException err) {
+ log.error("Cannot resolve \"" + baseRevision + "\" in project \""
+ + projectName.get() + "\"", err);
+ throw new InvalidRevisionException();
+ } catch (RevisionSyntaxException err) {
+ log.error("Invalid revision syntax \"" + baseRevision + "\"", err);
+ throw new InvalidRevisionException();
+ }
+ }
+
+ private RevWalk verifyConnected(final Repository repo, final ObjectId revid)
+ throws InvalidRevisionException {
+ try {
+ final ObjectWalk rw = new ObjectWalk(repo);
+ try {
+ rw.markStart(rw.parseCommit(revid));
+ } catch (IncorrectObjectTypeException err) {
+ throw new InvalidRevisionException();
+ }
+ for (final Ref r : repo.getAllRefs().values()) {
+ try {
+ rw.markUninteresting(rw.parseAny(r.getObjectId()));
+ } catch (MissingObjectException err) {
+ continue;
+ }
+ }
+ rw.checkConnectivity();
+ return rw;
+ } catch (IncorrectObjectTypeException err) {
+ throw new InvalidRevisionException();
+ } catch (MissingObjectException err) {
+ throw new InvalidRevisionException();
+ } catch (IOException err) {
+ log.error("Repository \"" + repo.getDirectory()
+ + "\" may be corrupt; suggest running git fsck", err);
+ throw new InvalidRevisionException();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 5494146..c5217c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -29,12 +29,16 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.group.GroupsCollection;
import com.google.gerrit.server.project.CreateProject.Input;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
import java.util.List;
@RequiresCapability(GlobalCapability.CREATE_PROJECT)
@@ -52,6 +56,7 @@
InheritableBoolean useSignedOffBy;
InheritableBoolean useContentMerge;
InheritableBoolean requireChangeId;
+ String maxObjectSizeLimit;
}
static interface Factory {
@@ -79,7 +84,7 @@
@Override
public Object apply(TopLevelResource resource, Input input)
throws BadRequestException, UnprocessableEntityException,
- ProjectCreationFailedException {
+ ProjectCreationFailedException, IOException {
if (input == null) {
input = new Input();
}
@@ -117,6 +122,12 @@
input.useContentMerge, InheritableBoolean.INHERIT);
args.changeIdRequired =
Objects.firstNonNull(input.requireChangeId, InheritableBoolean.INHERIT);
+ try {
+ args.maxObjectSizeLimit =
+ ProjectConfig.validMaxObjectSizeLimit(input.maxObjectSizeLimit);
+ } catch (ConfigInvalidException e) {
+ throw new BadRequestException(e.getMessage());
+ }
Project p = createProjectFactory.create(args).createProject();
return Response.created(json.format(p));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
index 7bbd2e7..ea20cea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -35,6 +35,7 @@
public InheritableBoolean contentMerge;
public InheritableBoolean changeIdRequired;
public boolean createEmptyCommit;
+ public String maxObjectSizeLimit;
public CreateProjectArgs() {
contributorAgreements = InheritableBoolean.INHERIT;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
new file mode 100644
index 0000000..a41c197
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.DeleteBranch.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+public class DeleteBranch implements RestModifyView<BranchResource, Input>{
+ private static final Logger log = LoggerFactory.getLogger(DeleteBranch.class);
+
+ static class Input {
+ }
+
+ private final IdentifiedUser identifiedUser;
+ private final GitRepositoryManager repoManager;
+ private final Provider<ReviewDb> dbProvider;
+ private final GitReferenceUpdated referenceUpdated;
+ private final ChangeHooks hooks;
+
+ @Inject
+ DeleteBranch(IdentifiedUser identifiedUser, GitRepositoryManager repoManager,
+ Provider<ReviewDb> dbProvider, GitReferenceUpdated referenceUpdated,
+ ChangeHooks hooks) {
+ this.identifiedUser = identifiedUser;
+ this.repoManager = repoManager;
+ this.dbProvider = dbProvider;
+ this.referenceUpdated = referenceUpdated;
+ this.hooks = hooks;
+ }
+
+ @Override
+ public Object apply(BranchResource rsrc, Input input) throws AuthException,
+ ResourceConflictException, OrmException, IOException {
+ if (!rsrc.getControl().controlForRef(rsrc.getBranchKey()).canDelete()) {
+ throw new AuthException("Cannot delete branch");
+ }
+ if (dbProvider.get().changes().byBranchOpenAll(rsrc.getBranchKey())
+ .iterator().hasNext()) {
+ throw new ResourceConflictException("branch " + rsrc.getBranchKey()
+ + " has open changes");
+ }
+
+ Repository r = repoManager.openRepository(rsrc.getNameKey());
+ try {
+ RefUpdate.Result result;
+ RefUpdate u;
+ try {
+ u = r.updateRef(rsrc.getRef());
+ u.setForceUpdate(true);
+ result = u.delete();
+ } catch (IOException e) {
+ log.error("Cannot delete " + rsrc.getBranchKey(), e);
+ throw e;
+ }
+
+ switch (result) {
+ case NEW:
+ case NO_CHANGE:
+ case FAST_FORWARD:
+ case FORCED:
+ referenceUpdated.fire(rsrc.getNameKey(), u);
+ hooks.doRefUpdatedHook(rsrc.getBranchKey(), u, identifiedUser.getAccount());
+ break;
+
+ case REJECTED_CURRENT_BRANCH:
+ log.warn("Cannot delete " + rsrc.getBranchKey() + ": " + result.name());
+ throw new ResourceConflictException("cannot delete current branch");
+
+ default:
+ log.error("Cannot delete " + rsrc.getBranchKey() + ": " + result.name());
+ throw new ResourceConflictException("cannot delete branch: " + result.name());
+ }
+ } finally {
+ r.close();
+ }
+ return Response.none();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
index d4d923e..ca7f6f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
@@ -18,8 +18,8 @@
import com.google.gerrit.common.data.GarbageCollectionResult;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.restapi.StreamingResponse;
import com.google.gerrit.server.git.GarbageCollection;
import com.google.gerrit.server.project.GarbageCollect.Input;
import com.google.inject.Inject;
@@ -43,15 +43,10 @@
}
@Override
- public StreamingResponse apply(final ProjectResource rsrc, Input input) {
- return new StreamingResponse() {
+ public BinaryResult apply(final ProjectResource rsrc, Input input) {
+ return new BinaryResult() {
@Override
- public String getContentType() {
- return "text/plain;charset=UTF-8";
- }
-
- @Override
- public void stream(OutputStream out) throws IOException {
+ public void writeTo(OutputStream out) throws IOException {
PrintWriter writer = new PrintWriter(
new OutputStreamWriter(out, Charsets.UTF_8)) {
@Override
@@ -88,6 +83,8 @@
writer.flush();
}
}
- };
+ }.setContentType("text/plain")
+ .setCharacterEncoding(Charsets.UTF_8.name())
+ .disableGzip();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
new file mode 100644
index 0000000..781cf01
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetBranch.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.project.ListBranches.BranchInfo;
+
+public class GetBranch implements RestReadView<BranchResource> {
+
+ @Override
+ public BranchInfo apply(BranchResource rsrc) {
+ return rsrc.getBranchInfo();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
new file mode 100644
index 0000000..1659cb7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetChildProject.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Option;
+
+public class GetChildProject implements RestReadView<ChildProjectResource> {
+ @Option(name = "--recursive", usage = "to list child projects recursively")
+ private boolean recursive;
+
+ private final ProjectJson json;
+
+ @Inject
+ GetChildProject(ProjectJson json) {
+ this.json = json;
+ }
+
+ @Override
+ public ProjectInfo apply(ChildProjectResource rsrc)
+ throws ResourceNotFoundException {
+ if (recursive || rsrc.isDirectChild()) {
+ return json.format(rsrc.getChild().getProject());
+ }
+ throw new ResourceNotFoundException(rsrc.getName());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
index cd5e5d7..cac9cdb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
@@ -14,81 +14,21 @@
package com.google.gerrit.server.project;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Maps;
import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
-import com.google.gerrit.server.git.GitRepositoryManager;
-
-import java.util.Map;
+import com.google.gerrit.server.git.TransferConfig;
+import com.google.inject.Inject;
public class GetConfig implements RestReadView<ProjectResource> {
+ private final TransferConfig config;
+
+ @Inject
+ public GetConfig(TransferConfig config) {
+ this.config = config;
+ }
+
@Override
public ConfigInfo apply(ProjectResource resource) {
- ConfigInfo result = new ConfigInfo();
- RefControl refConfig = resource.getControl()
- .controlForRef(GitRepositoryManager.REF_CONFIG);
- ProjectState state = resource.getControl().getProjectState();
- if (refConfig.isVisible()) {
- InheritedBooleanInfo useContributorAgreements = new InheritedBooleanInfo();
- InheritedBooleanInfo useSignedOffBy = new InheritedBooleanInfo();
- InheritedBooleanInfo useContentMerge = new InheritedBooleanInfo();
- InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
-
- useContributorAgreements.value = state.isUseContributorAgreements();
- useSignedOffBy.value = state.isUseSignedOffBy();
- useContentMerge.value = state.isUseContentMerge();
- requireChangeId.value = state.isRequireChangeID();
-
- Project p = state.getProject();
- useContributorAgreements.configuredValue = p.getUseContributorAgreements();
- useSignedOffBy.configuredValue = p.getUseSignedOffBy();
- useContentMerge.configuredValue = p.getUseContentMerge();
- requireChangeId.configuredValue = p.getRequireChangeID();
-
- ProjectState parentState = Iterables.getFirst(state.parents(), null);
- if (parentState != null) {
- useContributorAgreements.inheritedValue = parentState.isUseContributorAgreements();
- useSignedOffBy.inheritedValue = parentState.isUseSignedOffBy();
- useContentMerge.inheritedValue = parentState.isUseContentMerge();
- requireChangeId.inheritedValue = parentState.isRequireChangeID();
- }
-
- result.useContributorAgreements = useContributorAgreements;
- result.useSignedOffBy = useSignedOffBy;
- result.useContentMerge = useContentMerge;
- result.requireChangeId = requireChangeId;
- }
-
- // commentlinks are visible to anyone, as they are used for linkification
- // on the client side.
- result.commentlinks = Maps.newLinkedHashMap();
- for (CommentLinkInfo cl : state.getCommentLinks()) {
- result.commentlinks.put(cl.name, cl);
- }
-
- // Themes are visible to anyone, as they are rendered client-side.
- result.theme = state.getTheme();
- return result;
- }
-
- public static class ConfigInfo {
- public final String kind = "gerritcodereview#project_config";
-
- public InheritedBooleanInfo useContributorAgreements;
- public InheritedBooleanInfo useContentMerge;
- public InheritedBooleanInfo useSignedOffBy;
- public InheritedBooleanInfo requireChangeId;
-
- public Map<String, CommentLinkInfo> commentlinks;
- public ThemeInfo theme;
- }
-
- public static class InheritedBooleanInfo {
- public Boolean value;
- public InheritableBoolean configuredValue;
- public Boolean inheritedValue;
+ return new ConfigInfo(resource.getControl().getProjectState(), config);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
new file mode 100644
index 0000000..b01c757
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ListBranches implements RestReadView<ProjectResource> {
+
+ private final GitRepositoryManager repoManager;
+
+ @Inject
+ public ListBranches(GitRepositoryManager repoManager) {
+ this.repoManager = repoManager;
+ }
+
+ @Override
+ public List<BranchInfo> apply(ProjectResource rsrc)
+ throws ResourceNotFoundException, IOException {
+ List<BranchInfo> branches = Lists.newArrayList();
+
+ BranchInfo headBranch = null;
+ BranchInfo configBranch = null;
+ final Set<String> targets = Sets.newHashSet();
+
+ final Repository db;
+ try {
+ db = repoManager.openRepository(rsrc.getNameKey());
+ } catch (RepositoryNotFoundException noGitRepository) {
+ throw new ResourceNotFoundException();
+ }
+
+ try {
+ final Map<String, Ref> all = db.getAllRefs();
+
+ if (!all.containsKey(Constants.HEAD)) {
+ // The branch pointed to by HEAD doesn't exist yet, so getAllRefs
+ // filtered it out. If we ask for it individually we can find the
+ // underlying target and put it into the map anyway.
+ //
+ try {
+ Ref head = db.getRef(Constants.HEAD);
+ if (head != null) {
+ all.put(Constants.HEAD, head);
+ }
+ } catch (IOException e) {
+ // Ignore the failure reading HEAD.
+ }
+ }
+
+ for (final Ref ref : all.values()) {
+ if (ref.isSymbolic()) {
+ targets.add(ref.getTarget().getName());
+ }
+ }
+
+ for (final Ref ref : all.values()) {
+ if (ref.isSymbolic()) {
+ // A symbolic reference to another branch, instead of
+ // showing the resolved value, show the name it references.
+ //
+ String target = ref.getTarget().getName();
+ RefControl targetRefControl = rsrc.getControl().controlForRef(target);
+ if (!targetRefControl.isVisible()) {
+ continue;
+ }
+ if (target.startsWith(Constants.R_HEADS)) {
+ target = target.substring(Constants.R_HEADS.length());
+ }
+
+ BranchInfo b = new BranchInfo();
+ b.ref = ref.getName();
+ b.revision = target;
+
+ if (Constants.HEAD.equals(ref.getName())) {
+ b.setCanDelete(false);
+ headBranch = b;
+ } else {
+ b.setCanDelete(targetRefControl.canDelete());
+ branches.add(b);
+ }
+ continue;
+ }
+
+ final RefControl refControl = rsrc.getControl().controlForRef(ref.getName());
+ if (refControl.isVisible()) {
+ if (ref.getName().startsWith(Constants.R_HEADS)) {
+ branches.add(createBranchInfo(ref, refControl, targets));
+ } else if (GitRepositoryManager.REF_CONFIG.equals(ref.getName())) {
+ configBranch = createBranchInfo(ref, refControl, targets);
+ }
+ }
+ }
+ } finally {
+ db.close();
+ }
+ Collections.sort(branches, new Comparator<BranchInfo>() {
+ @Override
+ public int compare(final BranchInfo a, final BranchInfo b) {
+ return a.ref.compareTo(b.ref);
+ }
+ });
+ if (configBranch != null) {
+ branches.add(0, configBranch);
+ }
+ if (headBranch != null) {
+ branches.add(0, headBranch);
+ }
+ return branches;
+ }
+
+ private static BranchInfo createBranchInfo(Ref ref, RefControl refControl,
+ Set<String> targets) {
+ BranchInfo b = new BranchInfo();
+ b.ref = ref.getName();
+ if (ref.getObjectId() != null) {
+ b.revision = ref.getObjectId().name();
+ }
+ b.setCanDelete(!targets.contains(ref.getName()) && refControl.canDelete());
+ return b;
+ }
+
+ public static class BranchInfo {
+ public String ref;
+ public String revision;
+ public Boolean canDelete;
+
+ void setCanDelete(boolean canDelete) {
+ this.canDelete = canDelete ? true : null;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
new file mode 100644
index 0000000..d4b84dd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Option;
+
+import java.util.List;
+import java.util.Map;
+
+public class ListChildProjects implements RestReadView<ProjectResource> {
+
+ @Option(name = "--recursive", usage = "to list child projects recursively")
+ private boolean recursive;
+
+ private final ProjectCache projectCache;
+ private final AllProjectsName allProjects;
+ private final ProjectJson json;
+ private final ProjectNode.Factory projectNodeFactory;
+
+ @Inject
+ ListChildProjects(ProjectCache projectCache, AllProjectsName allProjects,
+ ProjectJson json, ProjectNode.Factory projectNodeFactory) {
+ this.projectCache = projectCache;
+ this.allProjects = allProjects;
+ this.json = json;
+ this.projectNodeFactory = projectNodeFactory;
+ }
+
+ @Override
+ public List<ProjectInfo> apply(ProjectResource rsrc) {
+ if (recursive) {
+ return getChildProjectsRecursively(rsrc.getNameKey(),
+ rsrc.getControl().getCurrentUser());
+ } else {
+ return getDirectChildProjects(rsrc.getNameKey());
+ }
+ }
+
+ private List<ProjectInfo> getDirectChildProjects(Project.NameKey parent) {
+ List<ProjectInfo> childProjects = Lists.newArrayList();
+ for (Project.NameKey projectName : projectCache.all()) {
+ ProjectState e = projectCache.get(projectName);
+ if (e == null) {
+ // If we can't get it from the cache, pretend it's not present.
+ continue;
+ }
+ if (parent.equals(e.getProject().getParent(allProjects))) {
+ childProjects.add(json.format(e.getProject()));
+ }
+ }
+ return childProjects;
+ }
+
+ private List<ProjectInfo> getChildProjectsRecursively(Project.NameKey parent,
+ CurrentUser user) {
+ Map<Project.NameKey, ProjectNode> projects = Maps.newHashMap();
+ for (Project.NameKey name : projectCache.all()) {
+ ProjectState p = projectCache.get(name);
+ if (p == null) {
+ // If we can't get it from the cache, pretend it's not present.
+ continue;
+ }
+ projects.put(name, projectNodeFactory.create(p.getProject(),
+ p.controlFor(user).isVisible()));
+ }
+ for (ProjectNode key : projects.values()) {
+ ProjectNode node = projects.get(key.getParentName());
+ if (node != null) {
+ node.addChild(key);
+ }
+ }
+ return getChildProjectsRecursively(projects.get(parent));
+ }
+
+ private List<ProjectInfo> getChildProjectsRecursively(ProjectNode p) {
+ List<ProjectInfo> allChildren = Lists.newArrayList();
+ for (ProjectNode c : p.getChildren()) {
+ if (c.isVisible()) {
+ allChildren.add(json.format(c.getProject()));
+ allChildren.addAll(getChildProjectsRecursively(c));
+ }
+ }
+ return allChildren;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index 1c61d96..0ef875e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -14,12 +14,13 @@
package com.google.gerrit.server.project;
+import static com.google.gerrit.server.project.BranchResource.BRANCH_KIND;
+import static com.google.gerrit.server.project.ChildProjectResource.CHILD_PROJECT_KIND;
import static com.google.gerrit.server.project.DashboardResource.DASHBOARD_KIND;
import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.gerrit.server.project.CreateProject;
import com.google.inject.assistedinject.FactoryModuleBuilder;
public class Module extends RestApiModule {
@@ -29,6 +30,8 @@
bind(DashboardsCollection.class);
DynamicMap.mapOf(binder(), PROJECT_KIND);
+ DynamicMap.mapOf(binder(), CHILD_PROJECT_KIND);
+ DynamicMap.mapOf(binder(), BRANCH_KIND);
DynamicMap.mapOf(binder(), DASHBOARD_KIND);
put(PROJECT_KIND).to(PutProject.class);
@@ -40,12 +43,21 @@
get(PROJECT_KIND, "parent").to(GetParent.class);
put(PROJECT_KIND, "parent").to(SetParent.class);
+ child(PROJECT_KIND, "children").to(ChildProjectsCollection.class);
+ get(CHILD_PROJECT_KIND).to(GetChildProject.class);
+
get(PROJECT_KIND, "HEAD").to(GetHead.class);
put(PROJECT_KIND, "HEAD").to(SetHead.class);
get(PROJECT_KIND, "statistics.git").to(GetStatistics.class);
post(PROJECT_KIND, "gc").to(GarbageCollect.class);
+ child(PROJECT_KIND, "branches").to(BranchesCollection.class);
+ put(BRANCH_KIND).to(PutBranch.class);
+ get(BRANCH_KIND).to(GetBranch.class);
+ delete(BRANCH_KIND).to(DeleteBranch.class);
+ install(new FactoryModuleBuilder().build(CreateBranch.Factory.class));
+
child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class);
get(DASHBOARD_KIND).to(GetDashboard.class);
put(DASHBOARD_KIND).to(SetDashboard.class);
@@ -53,5 +65,6 @@
install(new FactoryModuleBuilder().build(CreateProject.Factory.class));
get(PROJECT_KIND, "config").to(GetConfig.class);
+ put(PROJECT_KIND, "config").to(PutConfig.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
index d68725f..86410af 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
@@ -118,7 +118,11 @@
}
};
for (NewProjectCreatedListener l : createdListener) {
- l.onNewProjectCreated(event);
+ try {
+ l.onNewProjectCreated(event);
+ } catch (RuntimeException e) {
+ log.warn("Failure in NewProjectCreatedListener", e);
+ }
}
final RefUpdate u = repo.updateRef(Constants.HEAD);
@@ -181,6 +185,7 @@
newProject.setUseSignedOffBy(createProjectArgs.signedOffBy);
newProject.setUseContentMerge(createProjectArgs.contentMerge);
newProject.setRequireChangeID(createProjectArgs.changeIdRequired);
+ newProject.setMaxObjectSizeLimit(createProjectArgs.maxObjectSizeLimit);
if (createProjectArgs.newParent != null) {
newProject.setParentName(createProjectArgs.newParent.getProject()
.getNameKey());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
index 483ecaf..6647652 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
@@ -77,7 +77,7 @@
boolean perUser = false;
Map<AccessSection, Project.NameKey> sectionToProject = Maps.newLinkedHashMap();
- for (SectionMatcher matcher : matcherList) {
+ for (SectionMatcher sm : matcherList) {
// If the matcher has to expand parameters and its prefix matches the
// reference there is a very good chance the reference is actually user
// specific, even if the matcher does not match the reference. Since its
@@ -91,12 +91,12 @@
// references are usually less frequent than the non-user references.
//
if (username != null && !perUser
- && matcher instanceof SectionMatcher.ExpandParameters) {
- perUser = ((SectionMatcher.ExpandParameters) matcher).matchPrefix(ref);
+ && sm.matcher instanceof RefPatternMatcher.ExpandParameters) {
+ perUser = ((RefPatternMatcher.ExpandParameters) sm.matcher).matchPrefix(ref);
}
- if (matcher.match(ref, username)) {
- sectionToProject.put(matcher.section, matcher.project);
+ if (sm.match(ref, username)) {
+ sectionToProject.put(sm.section, sm.project);
}
}
List<AccessSection> sections = Lists.newArrayList(sectionToProject.keySet());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index 697b838..0e5cecb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -17,6 +17,7 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
+import java.io.IOException;
import java.util.Set;
/** Cache of project information, including access rights. */
@@ -28,13 +29,27 @@
* Get the cached data for a project by its unique name.
*
* @param projectName name of the project.
- * @return the cached data; null if no such project exists.
+ * @return the cached data; null if no such project exists or a error occured.
+ * @see #checkedGet(com.google.gerrit.reviewdb.client.Project.NameKey)
*/
public ProjectState get(Project.NameKey projectName);
+ /**
+ * Get the cached data for a project by its unique name.
+ *
+ * @param projectName name of the project.
+ * @throws IOException when there was an error.
+ * @return the cached data; null if no such project exists.
+ */
+ public ProjectState checkedGet(Project.NameKey projectName)
+ throws IOException;
+
/** Invalidate the cached information about the given project. */
public void evict(Project p);
+ /** Invalidate the cached information about the given project. */
+ public void evict(Project.NameKey p);
+
/**
* Remove information about the given project from the cache. It will no
* longer be returned from {@link #all()}.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 272c128..8ccbca3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.project;
+import com.google.common.base.Throwables;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Sets;
@@ -34,6 +35,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
@@ -101,13 +103,18 @@
return state;
}
- /**
- * Get the cached data for a project by its unique name.
- *
- * @param projectName name of the project.
- * @return the cached data; null if no such project exists.
- */
+ @Override
public ProjectState get(final Project.NameKey projectName) {
+ try {
+ return checkedGet(projectName);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public ProjectState checkedGet(Project.NameKey projectName)
+ throws IOException {
if (projectName == null) {
return null;
}
@@ -121,18 +128,27 @@
} catch (ExecutionException e) {
if (!(e.getCause() instanceof RepositoryNotFoundException)) {
log.warn(String.format("Cannot read project %s", projectName.get()), e);
+ Throwables.propagateIfInstanceOf(e.getCause(), IOException.class);
+ throw new IOException(e);
}
return null;
}
}
- /** Invalidate the cached information about the given project. */
+ @Override
public void evict(final Project p) {
if (p != null) {
byName.invalidate(p.getNameKey().get());
}
}
+ /** Invalidate the cached information about the given project. */
+ public void evict(final Project.NameKey p) {
+ if (p != null) {
+ byName.invalidate(p.get());
+ }
+ }
+
@Override
public void remove(final Project p) {
listLock.lock();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 56d4be3..479f377 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -38,6 +38,7 @@
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -62,13 +63,25 @@
}
public ProjectControl controlFor(Project.NameKey nameKey, CurrentUser user)
- throws NoSuchProjectException {
- final ProjectState p = projectCache.get(nameKey);
+ throws NoSuchProjectException, IOException {
+ final ProjectState p = projectCache.checkedGet(nameKey);
if (p == null) {
throw new NoSuchProjectException(nameKey);
}
return p.controlFor(user);
}
+
+ public ProjectControl validateFor(Project.NameKey nameKey, int need,
+ CurrentUser user) throws NoSuchProjectException, IOException {
+ final ProjectControl c = controlFor(nameKey, user);
+ if ((need & VISIBLE) == VISIBLE && c.isVisible()) {
+ return c;
+ }
+ if ((need & OWNER) == OWNER && c.isOwner()) {
+ return c;
+ }
+ throw new NoSuchProjectException(nameKey);
+ }
}
public static class Factory {
@@ -204,6 +217,20 @@
|| isOwnerAnyRef());
}
+ public boolean canUpload() {
+ for (SectionMatcher matcher : access()) {
+ AccessSection section = matcher.section;
+ if (section.getName().startsWith("refs/for/")) {
+ Permission permission = section.getPermission(Permission.PUSH);
+ if (permission != null
+ && controlForRef(section.getName()).canPerform(Permission.PUSH)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
/** Can this user see all the refs in this projects? */
public boolean allRefsAreVisible() {
return allRefsAreVisibleExcept(Collections.<String> emptySet());
@@ -269,7 +296,7 @@
}
private Capable verifyActiveContributorAgreement() {
- if (! (user instanceof IdentifiedUser)) {
+ if (! (user.isIdentifiedUser())) {
return new Capable("Must be logged in to verify Contributor Agreement");
}
final IdentifiedUser iUser = (IdentifiedUser) user;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java
index 1c4d7c4..5b4b334 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java
@@ -57,6 +57,10 @@
return allProjectsName.equals(project.getNameKey());
}
+ public Project getProject() {
+ return project;
+ }
+
@Override
public String getDisplayName() {
return project.getName();
@@ -68,7 +72,7 @@
}
@Override
- public SortedSet<? extends TreeNode> getChildren() {
+ public SortedSet<? extends ProjectNode> getChildren() {
return children;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
index aeca0e8..f4449f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
@@ -25,10 +25,14 @@
private final ProjectControl control;
- ProjectResource(ProjectControl control) {
+ public ProjectResource(ProjectControl control) {
this.control = control;
}
+ ProjectResource(ProjectResource rsrc) {
+ this.control = rsrc.getControl();
+ }
+
public String getName() {
return control.getProject().getName();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 6f80841..800fa6e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -216,6 +216,10 @@
return config;
}
+ public long getMaxObjectSizeLimit() {
+ return config.getMaxObjectSizeLimit();
+ }
+
/** Get the sections that pertain only to this project. */
List<SectionMatcher> getLocalAccessSections() {
List<SectionMatcher> sm = localAccessSections;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
index 4c00439..6e9c5d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
@@ -28,6 +28,8 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
+import java.io.IOException;
+
public class ProjectsCollection implements
RestCollection<TopLevelResource, ProjectResource>,
AcceptsCreate<TopLevelResource> {
@@ -56,7 +58,7 @@
@Override
public ProjectResource parse(TopLevelResource parent, IdString id)
- throws ResourceNotFoundException {
+ throws ResourceNotFoundException, IOException {
ProjectResource rsrc = _parse(id.get());
if (rsrc == null) {
throw new ResourceNotFoundException(id);
@@ -71,8 +73,10 @@
* @return the project
* @throws UnprocessableEntityException thrown if the project ID cannot be
* resolved or if the project is not visible to the calling user
+ * @throws IOException thrown when there is an error.
*/
- public ProjectResource parse(String id) throws UnprocessableEntityException {
+ public ProjectResource parse(String id)
+ throws UnprocessableEntityException, IOException {
ProjectResource rsrc = _parse(id);
if (rsrc == null) {
throw new UnprocessableEntityException(String.format(
@@ -81,7 +85,7 @@
return rsrc;
}
- private ProjectResource _parse(String id) {
+ private ProjectResource _parse(String id) throws IOException {
ProjectControl ctl;
try {
ctl = controlFactory.controlFor(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java
new file mode 100644
index 0000000..2cd5659
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.project.CreateBranch.Input;
+
+public class PutBranch implements RestModifyView<BranchResource, Input> {
+
+ @Override
+ public Object apply(BranchResource rsrc, Input input)
+ throws ResourceConflictException {
+ throw new ResourceConflictException("Branch \"" + rsrc.getBranchInfo().ref
+ + "\" already exists");
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
new file mode 100644
index 0000000..fd091f0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -0,0 +1,143 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.project.PutConfig.Input;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
+public class PutConfig implements RestModifyView<ProjectResource, Input> {
+ public static class Input {
+ public String description;
+ public InheritableBoolean useContributorAgreements;
+ public InheritableBoolean useContentMerge;
+ public InheritableBoolean useSignedOffBy;
+ public InheritableBoolean requireChangeId;
+ public String maxObjectSizeLimit;
+ public SubmitType submitType;
+ public Project.State state;
+ }
+
+ private final MetaDataUpdate.User metaDataUpdateFactory;
+ private final ProjectCache projectCache;
+ private final Provider<CurrentUser> self;
+ private final ProjectState.Factory projectStateFactory;
+ private final TransferConfig config;
+
+ @Inject
+ PutConfig(MetaDataUpdate.User metaDataUpdateFactory,
+ ProjectCache projectCache,
+ Provider<CurrentUser> self,
+ ProjectState.Factory projectStateFactory,
+ TransferConfig config) {
+ this.metaDataUpdateFactory = metaDataUpdateFactory;
+ this.projectCache = projectCache;
+ this.self = self;
+ this.projectStateFactory = projectStateFactory;
+ this.config = config;
+ }
+
+ @Override
+ public ConfigInfo apply(ProjectResource rsrc, Input input)
+ throws ResourceNotFoundException, BadRequestException,
+ ResourceConflictException {
+ Project.NameKey projectName = rsrc.getNameKey();
+ if (!rsrc.getControl().isOwner()) {
+ throw new ResourceNotFoundException(projectName.get());
+ }
+
+ if (input == null) {
+ throw new BadRequestException("config is required");
+ }
+
+ final MetaDataUpdate md;
+ try {
+ md = metaDataUpdateFactory.create(projectName);
+ } catch (RepositoryNotFoundException notFound) {
+ throw new ResourceNotFoundException(projectName.get());
+ } catch (IOException e) {
+ throw new ResourceNotFoundException(projectName.get(), e);
+ }
+ try {
+ ProjectConfig projectConfig = ProjectConfig.read(md);
+ Project p = projectConfig.getProject();
+
+ p.setDescription(Strings.emptyToNull(input.description));
+
+ if (input.useContributorAgreements != null) {
+ p.setUseContributorAgreements(input.useContributorAgreements);
+ }
+ if (input.useContentMerge != null) {
+ p.setUseContentMerge(input.useContentMerge);
+ }
+ if (input.useSignedOffBy != null) {
+ p.setUseSignedOffBy(input.useSignedOffBy);
+ }
+ if (input.requireChangeId != null) {
+ p.setRequireChangeID(input.requireChangeId);
+ }
+
+ if (input.maxObjectSizeLimit != null) {
+ p.setMaxObjectSizeLimit(input.maxObjectSizeLimit);
+ }
+
+ if (input.submitType != null) {
+ p.setSubmitType(input.submitType);
+ }
+
+ if (input.state != null) {
+ p.setState(input.state);
+ }
+
+ md.setMessage("Modified project settings\n");
+ try {
+ projectConfig.commit(md);
+ (new PerRequestProjectControlCache(projectCache, self.get()))
+ .evict(projectConfig.getProject());
+ } catch (IOException e) {
+ if (e.getCause() instanceof ConfigInvalidException) {
+ throw new ResourceConflictException("Cannot update " + projectName
+ + ": " + e.getCause().getMessage());
+ } else {
+ throw new ResourceConflictException("Cannot update " + projectName);
+ }
+ }
+ return new ConfigInfo(projectStateFactory.create(projectConfig), config);
+ } catch (ConfigInvalidException err) {
+ throw new ResourceConflictException("Cannot read project " + projectName, err);
+ } catch (IOException err) {
+ throw new ResourceConflictException("Cannot update project " + projectName, err);
+ } finally {
+ md.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 59b7670..0898789 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -262,7 +262,7 @@
final PersonIdent tagger = tag.getTaggerIdent();
if (tagger != null) {
boolean valid;
- if (getCurrentUser() instanceof IdentifiedUser) {
+ if (getCurrentUser().isIdentifiedUser()) {
final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
final String addr = tagger.getEmailAddress();
valid = user.getEmailAddresses().contains(addr);
@@ -306,6 +306,7 @@
switch (getCurrentUser().getAccessPath()) {
case REST_API:
case JSON_RPC:
+ case SSH_COMMAND:
return isOwner() || canPushWithForce();
case GIT:
@@ -395,7 +396,7 @@
/** The range of permitted values associated with a label permission. */
public PermissionRange getRange(String permission) {
- if (Permission.isLabel(permission)) {
+ if (Permission.hasRange(permission)) {
return toRange(permission, access(permission));
}
return null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPatternMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPatternMatcher.java
new file mode 100644
index 0000000..b71d194
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPatternMatcher.java
@@ -0,0 +1,122 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static com.google.gerrit.server.project.RefControl.isRE;
+import com.google.gerrit.common.data.ParameterizedString;
+import dk.brics.automaton.Automaton;
+import java.util.Collections;
+import java.util.regex.Pattern;
+
+abstract class RefPatternMatcher {
+ public static RefPatternMatcher getMatcher(String pattern) {
+ if (pattern.contains("${")) {
+ return new ExpandParameters(pattern);
+ } else if (isRE(pattern)) {
+ return new Regexp(pattern);
+ } else if (pattern.endsWith("/*")) {
+ return new Prefix(pattern.substring(0, pattern.length() - 1));
+ } else {
+ return new Exact(pattern);
+ }
+ }
+
+ abstract boolean match(String ref, String username);
+
+ private static class Exact extends RefPatternMatcher {
+ private final String expect;
+
+ Exact(String name) {
+ expect = name;
+ }
+
+ @Override
+ boolean match(String ref, String username) {
+ return expect.equals(ref);
+ }
+ }
+
+ private static class Prefix extends RefPatternMatcher {
+ private final String prefix;
+
+ Prefix(String pfx) {
+ prefix = pfx;
+ }
+
+ @Override
+ boolean match(String ref, String username) {
+ return ref.startsWith(prefix);
+ }
+ }
+
+ private static class Regexp extends RefPatternMatcher {
+ private final Pattern pattern;
+
+ Regexp(String re) {
+ pattern = Pattern.compile(re);
+ }
+
+ @Override
+ boolean match(String ref, String username) {
+ return pattern.matcher(ref).matches();
+ }
+ }
+
+ static class ExpandParameters extends RefPatternMatcher {
+ private final ParameterizedString template;
+ private final String prefix;
+
+ ExpandParameters(String pattern) {
+ template = new ParameterizedString(pattern);
+
+ if (isRE(pattern)) {
+ // Replace ${username} with ":USERNAME:" as : is not legal
+ // in a reference and the string :USERNAME: is not likely to
+ // be a valid part of the regex. This later allows the pattern
+ // prefix to be clipped, saving time on evaluation.
+ Automaton am =
+ RefControl.toRegExp(
+ template.replace(Collections.singletonMap("username",
+ ":USERNAME:"))).toAutomaton();
+ String rePrefix = am.getCommonPrefix();
+ prefix = rePrefix.substring(0, rePrefix.indexOf(":USERNAME:"));
+ } else {
+ prefix = pattern.substring(0, pattern.indexOf("${"));
+ }
+ }
+
+ @Override
+ boolean match(String ref, String username) {
+ if (!ref.startsWith(prefix) || username == null) {
+ return false;
+ }
+
+ String u;
+ if (isRE(template.getPattern())) {
+ u = username.replace(".", "\\.");
+ } else {
+ u = username;
+ }
+
+ RefPatternMatcher next =
+ getMatcher(template.replace(Collections.singletonMap("username", u)));
+ return next != null ? next.match(ref, username) : false;
+ }
+
+ boolean matchPrefix(String ref) {
+ return ref.startsWith(prefix);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
index 6f8af80..44c8b9a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
@@ -14,147 +14,38 @@
package com.google.gerrit.server.project;
-import static com.google.gerrit.server.project.RefControl.isRE;
-
import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.reviewdb.client.Project;
-import dk.brics.automaton.Automaton;
-
-import java.util.Collections;
-import java.util.regex.Pattern;
-
/**
* Matches an AccessSection against a reference name.
* <p>
* These matchers are "compiled" versions of the AccessSection name, supporting
* faster selection of which sections are relevant to any given input reference.
*/
-abstract class SectionMatcher {
+class SectionMatcher extends RefPatternMatcher {
static SectionMatcher wrap(Project.NameKey project, AccessSection section) {
String ref = section.getName();
if (AccessSection.isValid(ref)) {
- return wrap(project, ref, section);
+ return new SectionMatcher(project, section, getMatcher(ref));
} else {
return null;
}
}
- static SectionMatcher wrap(Project.NameKey project, String pattern,
- AccessSection section) {
- if (pattern.contains("${")) {
- return new ExpandParameters(project, pattern, section);
-
- } else if (isRE(pattern)) {
- return new Regexp(project, pattern, section);
-
- } else if (pattern.endsWith("/*")) {
- return new Prefix(project, pattern.substring(0, pattern.length() - 1),
- section);
-
- } else {
- return new Exact(project, pattern, section);
- }
- }
-
final Project.NameKey project;
final AccessSection section;
+ final RefPatternMatcher matcher;
- SectionMatcher(Project.NameKey project, AccessSection section) {
+ SectionMatcher(Project.NameKey project, AccessSection section,
+ RefPatternMatcher matcher) {
this.project = project;
this.section = section;
+ this.matcher = matcher;
}
- abstract boolean match(String ref, String username);
-
- private static class Exact extends SectionMatcher {
- private final String expect;
-
- Exact(Project.NameKey project, String name, AccessSection section) {
- super(project, section);
- expect = name;
- }
-
- @Override
- boolean match(String ref, String username) {
- return expect.equals(ref);
- }
- }
-
- private static class Prefix extends SectionMatcher {
- private final String prefix;
-
- Prefix(Project.NameKey project, String pfx, AccessSection section) {
- super(project, section);
- prefix = pfx;
- }
-
- @Override
- boolean match(String ref, String username) {
- return ref.startsWith(prefix);
- }
- }
-
- private static class Regexp extends SectionMatcher {
- private final Pattern pattern;
-
- Regexp(Project.NameKey project, String re, AccessSection section) {
- super(project, section);
- pattern = Pattern.compile(re);
- }
-
- @Override
- boolean match(String ref, String username) {
- return pattern.matcher(ref).matches();
- }
- }
-
- static class ExpandParameters extends SectionMatcher {
- private final ParameterizedString template;
- private final String prefix;
-
- ExpandParameters(Project.NameKey project, String pattern,
- AccessSection section) {
- super(project, section);
- template = new ParameterizedString(pattern);
-
- if (isRE(pattern)) {
- // Replace ${username} with ":USERNAME:" as : is not legal
- // in a reference and the string :USERNAME: is not likely to
- // be a valid part of the regex. This later allows the pattern
- // prefix to be clipped, saving time on evaluation.
- Automaton am = RefControl.toRegExp(
- template.replace(Collections.singletonMap("username", ":USERNAME:")))
- .toAutomaton();
- String rePrefix = am.getCommonPrefix();
- prefix = rePrefix.substring(0, rePrefix.indexOf(":USERNAME:"));
- } else {
- prefix = pattern.substring(0, pattern.indexOf("${"));
- }
- }
-
- @Override
- boolean match(String ref, String username) {
- if (!ref.startsWith(prefix) || username == null) {
- return false;
- }
-
- String u;
- if (isRE(template.getPattern())) {
- u = username.replace(".", "\\.");
- } else {
- u = username;
- }
-
- SectionMatcher next = wrap(project,
- template.replace(Collections.singletonMap("username", u)),
- section);
- return next != null ? next.match(ref, username) : false;
- }
-
- boolean matchPrefix(String ref) {
- return ref.startsWith(prefix);
- }
+ @Override
+ boolean match(String ref, String username) {
+ return this.matcher.match(ref, username);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
index db879de..aeb92d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
@@ -10,7 +10,7 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
-// limitations under the License.package com.google.gerrit.server.git;
+// limitations under the License.
package com.google.gerrit.server.project;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
index 3e7a42f..2f8e26d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
@@ -25,6 +25,7 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.SetHead.Input;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Constants;
@@ -40,10 +41,10 @@
}
private final GitRepositoryManager repoManager;
- private final IdentifiedUser identifiedUser;
+ private final Provider<IdentifiedUser> identifiedUser;
@Inject
- SetHead(GitRepositoryManager repoManager, IdentifiedUser identifiedUser) {
+ SetHead(GitRepositoryManager repoManager, Provider<IdentifiedUser> identifiedUser) {
this.repoManager = repoManager;
this.identifiedUser = identifiedUser;
}
@@ -73,7 +74,7 @@
if (!repo.getRef(Constants.HEAD).getTarget().getName().equals(ref)) {
final RefUpdate u = repo.updateRef(Constants.HEAD, true);
- u.setRefLogIdent(identifiedUser.newRefLogIdent());
+ u.setRefLogIdent(identifiedUser.get().newRefLogIdent());
RefUpdate.Result res = u.link(ref);
switch(res) {
case NO_CHANGE:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java
index 8362b572..4fcc4a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ThemeInfo.java
@@ -10,7 +10,7 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
-// limitations under the License.package com.google.gerrit.server.git;
+// limitations under the License.
package com.google.gerrit.server.project;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
index 096e12e..5966dde 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
@@ -45,9 +45,6 @@
c += p.getCost();
}
}
- if (t.size() < 2) {
- throw new IllegalArgumentException("Need at least two predicates");
- }
children = t;
cost = c;
}
@@ -101,7 +98,7 @@
}
@Override
- public final String toString() {
+ public String toString() {
final StringBuilder r = new StringBuilder();
r.append("(");
for (int i = 0; i < getChildCount(); i++) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java
deleted file mode 100644
index bea4da12..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query;
-
-import org.eclipse.jgit.lib.AbbreviatedObjectId;
-import org.eclipse.jgit.lib.ObjectId;
-
-
-/** Predicate for a field of {@link ObjectId}. */
-public abstract class ObjectIdPredicate<T> extends OperatorPredicate<T> {
- private final AbbreviatedObjectId id;
-
- public ObjectIdPredicate(final String name, final AbbreviatedObjectId id) {
- super(name, id.name());
- this.id = id;
- }
-
- public boolean isComplete() {
- return id.isComplete();
- }
-
- public AbbreviatedObjectId abbreviated() {
- return id;
- }
-
- public ObjectId full() {
- return id.toObjectId();
- }
-
- @Override
- public int hashCode() {
- return getOperator().hashCode() * 31 + id.hashCode();
- }
-
- @Override
- public boolean equals(Object other) {
- if (other instanceof ObjectIdPredicate) {
- final ObjectIdPredicate<?> p = (ObjectIdPredicate<?>) other;
- return getOperator().equals(p.getOperator()) && id.equals(p.id);
- }
- return false;
- }
-
- @Override
- public String toString() {
- return getOperator() + ":" + id.name();
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
index 57d21b1..8a0ac68 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
@@ -45,9 +45,6 @@
c += p.getCost();
}
}
- if (t.size() < 2) {
- throw new IllegalArgumentException("Need at least two predicates");
- }
children = t;
cost = c;
}
@@ -101,7 +98,7 @@
}
@Override
- public final String toString() {
+ public String toString() {
final StringBuilder r = new StringBuilder();
r.append("(");
for (int i = 0; i < getChildCount(); i++) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
index 5aa6cdc..a276992 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
@@ -109,6 +109,56 @@
}
}
+ /**
+ * Locate a predicate in the predicate tree.
+ *
+ * @param p the predicate to find.
+ * @param clazz type of the predicate instance.
+ * @return the predicate, null if not found.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T, P extends Predicate<T>> P find(Predicate<T> p, Class<P> clazz) {
+ if (clazz.isAssignableFrom(p.getClass())) {
+ return (P) p;
+ }
+
+ for (Predicate<T> c : p.getChildren()) {
+ P r = find(c, clazz);
+ if (r != null) {
+ return r;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Locate a predicate in the predicate tree.
+ *
+ * @param p the predicate to find.
+ * @param clazz type of the predicate instance.
+ * @param name name of the operator.
+ * @return the predicate, null if not found.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T, P extends OperatorPredicate<T>> P find(Predicate<T> p,
+ Class<P> clazz, String name) {
+ if (p instanceof OperatorPredicate
+ && ((OperatorPredicate<?>) p).getOperator().equals(name)
+ && clazz.isAssignableFrom(p.getClass())) {
+ return (P) p;
+ }
+
+ for (Predicate<T> c : p.getChildren()) {
+ P r = find(c, clazz, name);
+ if (r != null) {
+ return r;
+ }
+ }
+
+ return null;
+ }
+
@SuppressWarnings("rawtypes")
private final Map<String, OperatorFactory> opFactories;
@@ -238,56 +288,6 @@
throw error("Unsupported query:" + value);
}
- /**
- * Locate a predicate in the predicate tree.
- *
- * @param p the predicate to find.
- * @param clazz type of the predicate instance.
- * @return the predicate, null if not found.
- */
- @SuppressWarnings("unchecked")
- public <P extends Predicate<T>> P find(Predicate<T> p, Class<P> clazz) {
- if (clazz.isAssignableFrom(p.getClass())) {
- return (P) p;
- }
-
- for (Predicate<T> c : p.getChildren()) {
- P r = find(c, clazz);
- if (r != null) {
- return r;
- }
- }
-
- return null;
- }
-
- /**
- * Locate a predicate in the predicate tree.
- *
- * @param p the predicate to find.
- * @param clazz type of the predicate instance.
- * @param name name of the operator.
- * @return the predicate, null if not found.
- */
- @SuppressWarnings("unchecked")
- public <P extends OperatorPredicate<T>> P find(Predicate<T> p,
- Class<P> clazz, String name) {
- if (p instanceof OperatorPredicate
- && ((OperatorPredicate<?>) p).getOperator().equals(name)
- && clazz.isAssignableFrom(p.getClass())) {
- return (P) p;
- }
-
- for (Predicate<T> c : p.getChildren()) {
- P r = find(c, clazz, name);
- if (r != null) {
- return r;
- }
- }
-
- return null;
- }
-
@SuppressWarnings("unchecked")
private Predicate<T>[] children(final Tree r) throws QueryParseException,
IllegalArgumentException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
index 2a6bae0..6088d8e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query;
+import com.google.common.collect.Lists;
import com.google.inject.name.Named;
import java.lang.annotation.Annotation;
@@ -27,7 +28,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Comparator;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
@@ -65,19 +66,13 @@
private final List<RewriteRule<T>> rewriteRules;
public Definition(Class<R> clazz, QueryBuilder<T> qb) {
- rewriteRules = new ArrayList<RewriteRule<T>>();
+ rewriteRules = Lists.newArrayList();
Class<?> c = clazz;
while (c != QueryRewriter.class) {
- final Method[] declared = c.getDeclaredMethods();
- Arrays.sort(declared, new Comparator<Method>() {
- @Override
- public int compare(Method o1, Method o2) {
- return o1.getName().compareTo(o2.getName());
- }
- });
+ Method[] declared = c.getDeclaredMethods();
for (Method m : declared) {
- final Rewrite rp = m.getAnnotation(Rewrite.class);
+ Rewrite rp = m.getAnnotation(Rewrite.class);
if ((m.getModifiers() & Modifier.ABSTRACT) != Modifier.ABSTRACT
&& (m.getModifiers() & Modifier.PUBLIC) == Modifier.PUBLIC
&& rp != null) {
@@ -86,6 +81,7 @@
}
c = c.getSuperclass();
}
+ Collections.sort(rewriteRules);
}
}
@@ -120,13 +116,22 @@
return Predicate.not(that);
}
+ protected Predicate<T> preRewrite(Predicate<T> in) {
+ return in;
+ }
+
/**
* Apply rewrites to a graph until it stops changing.
*
* @param in the graph to rewrite.
* @return the rewritten graph.
*/
- public Predicate<T> rewrite(Predicate<T> in) {
+ public final Predicate<T> rewrite(Predicate<T> in) {
+ in = preRewrite(in);
+ return rewriteImpl(in);
+ }
+
+ private Predicate<T> rewriteImpl(Predicate<T> in) {
Predicate<T> old;
do {
old = in;
@@ -135,7 +140,7 @@
if (old.equals(in) && in.getChildCount() > 0) {
List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
for (Predicate<T> p : in.getChildren()) {
- n.add(rewrite(p));
+ n.add(rewriteImpl(p));
}
n = removeDuplicates(n);
if (n.size() == 1 && (isAND(in) || isOR(in))) {
@@ -150,17 +155,17 @@
}
protected Predicate<T> replaceGenericNodes(final Predicate<T> in) {
- if (in.getClass() == NotPredicate.class) {
+ if (in instanceof NotPredicate) {
return not(replaceGenericNodes(in.getChild(0)));
- } else if (in.getClass() == AndPredicate.class) {
+ } else if (in instanceof AndPredicate) {
List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
for (Predicate<T> c : in.getChildren()) {
n.add(replaceGenericNodes(c));
}
return and(n);
- } else if (in.getClass() == OrPredicate.class) {
+ } else if (in instanceof OrPredicate) {
List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
for (Predicate<T> c : in.getChildren()) {
n.add(replaceGenericNodes(c));
@@ -331,7 +336,7 @@
}
/** Applies a rewrite rule to a Predicate. */
- protected interface RewriteRule<T> {
+ protected interface RewriteRule<T> extends Comparable<RewriteRule<T>> {
/**
* Apply a rewrite rule to the Predicate.
*
@@ -454,6 +459,15 @@
final String msg = "Cannot apply " + method.getName();
return new IllegalArgumentException(msg, e);
}
+
+ @Override
+ public int compareTo(RewriteRule<T> in) {
+ if (in instanceof MethodRewrite) {
+ return method.getName().compareTo(
+ ((MethodRewrite<T>) in).method.getName());
+ }
+ return 1;
+ }
}
private static <T> Predicate<T> removeDuplicates(Predicate<T> in) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
index d0c9e4c..5f457c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
@@ -20,31 +20,42 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.TimestampRangePredicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
-class AgePredicate extends OperatorPredicate<ChangeData> {
+import java.sql.Timestamp;
+
+public class AgePredicate extends TimestampRangePredicate<ChangeData> {
private final Provider<ReviewDb> dbProvider;
private final long cut;
AgePredicate(Provider<ReviewDb> dbProvider, String value) {
- super(ChangeQueryBuilder.FIELD_AGE, value);
+ super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_AGE, value);
this.dbProvider = dbProvider;
long s = ConfigUtil.getTimeUnit(getValue(), 0, SECONDS);
long ms = MILLISECONDS.convert(s, SECONDS);
- this.cut = (System.currentTimeMillis() - ms) + 1;
+ this.cut = System.currentTimeMillis() - ms;
+ }
+
+ public Timestamp getMinTimestamp() {
+ return new Timestamp(0);
+ }
+
+ public Timestamp getMaxTimestamp() {
+ return new Timestamp(cut);
}
long getCut() {
- return cut;
+ return cut + 1;
}
@Override
public boolean match(final ChangeData object) throws OrmException {
Change change = object.change(dbProvider);
- return change != null && change.getLastUpdatedOn().getTime() < cut;
+ return change != null && change.getLastUpdatedOn().getTime() <= cut;
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
index 7f726a7..5282b49 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
@@ -34,7 +34,8 @@
import java.util.Comparator;
import java.util.List;
-class AndSource extends AndPredicate<ChangeData> implements ChangeDataSource {
+public class AndSource extends AndPredicate<ChangeData>
+ implements ChangeDataSource {
private static final Comparator<Predicate<ChangeData>> CMP =
new Comparator<Predicate<ChangeData>>() {
@Override
@@ -75,7 +76,8 @@
private final Provider<ReviewDb> db;
private int cardinality = -1;
- AndSource(Provider<ReviewDb> db, Collection<? extends Predicate<ChangeData>> that) {
+ public AndSource(Provider<ReviewDb> db,
+ Collection<? extends Predicate<ChangeData>> that) {
super(sort(that));
this.db = db;
}
@@ -160,12 +162,15 @@
}
private ChangeDataSource source() {
+ int minCost = Integer.MAX_VALUE;
+ Predicate<ChangeData> s = null;
for (Predicate<ChangeData> p : getChildren()) {
- if (p instanceof ChangeDataSource) {
- return (ChangeDataSource) p;
+ if (p instanceof ChangeDataSource && p.getCost() < minCost) {
+ s = p;
+ minCost = p.getCost();
}
}
- return null;
+ return (ChangeDataSource) s;
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
new file mode 100644
index 0000000..1d2c9d2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.query.IntPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryRewriter;
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+
+public abstract class BasicChangeRewrites extends QueryRewriter<ChangeData> {
+ protected static final ChangeQueryBuilder BUILDER = new ChangeQueryBuilder(
+ new ChangeQueryBuilder.Arguments( //
+ new InvalidProvider<ReviewDb>(), //
+ new InvalidProvider<ChangeQueryRewriter>(), //
+ null, null, null, null, null, //
+ null, null, null, null, null), null);
+
+ protected final Provider<ReviewDb> dbProvider;
+
+ protected BasicChangeRewrites(
+ Definition<ChangeData, ? extends QueryRewriter<ChangeData>> def,
+ Provider<ReviewDb> dbProvider) {
+ super(def);
+ this.dbProvider = dbProvider;
+ }
+
+ @Rewrite("-status:open")
+ @NoCostComputation
+ public Predicate<ChangeData> r00_notOpen() {
+ return ChangeStatusPredicate.closed(dbProvider);
+ }
+
+ @Rewrite("-status:closed")
+ @NoCostComputation
+ public Predicate<ChangeData> r00_notClosed() {
+ return ChangeStatusPredicate.open(dbProvider);
+ }
+
+ @SuppressWarnings("unchecked")
+ @NoCostComputation
+ @Rewrite("-status:merged")
+ public Predicate<ChangeData> r00_notMerged() {
+ return or(ChangeStatusPredicate.open(dbProvider),
+ new ChangeStatusPredicate(dbProvider, Change.Status.ABANDONED));
+ }
+
+ @SuppressWarnings("unchecked")
+ @NoCostComputation
+ @Rewrite("-status:abandoned")
+ public Predicate<ChangeData> r00_notAbandoned() {
+ return or(ChangeStatusPredicate.open(dbProvider),
+ new ChangeStatusPredicate(dbProvider, Change.Status.MERGED));
+ }
+
+ @SuppressWarnings("unchecked")
+ @NoCostComputation
+ @Rewrite("sortkey_before:z A=(age:*)")
+ public Predicate<ChangeData> r00_ageToSortKey(@Named("A") AgePredicate a) {
+ String cut = ChangeUtil.sortKey(a.getCut(), Integer.MAX_VALUE);
+ return and(new SortKeyPredicate.Before(dbProvider, cut), a);
+ }
+
+ @NoCostComputation
+ @Rewrite("A=(limit:*) B=(limit:*)")
+ public Predicate<ChangeData> r00_smallestLimit(
+ @Named("A") IntPredicate<ChangeData> a,
+ @Named("B") IntPredicate<ChangeData> b) {
+ return a.intValue() <= b.intValue() ? a : b;
+ }
+
+ @NoCostComputation
+ @Rewrite("A=(sortkey_before:*) B=(sortkey_before:*)")
+ public Predicate<ChangeData> r00_oldestSortKey(
+ @Named("A") SortKeyPredicate.Before a,
+ @Named("B") SortKeyPredicate.Before b) {
+ return a.getValue().compareTo(b.getValue()) <= 0 ? a : b;
+ }
+
+ @NoCostComputation
+ @Rewrite("A=(sortkey_after:*) B=(sortkey_after:*)")
+ public Predicate<ChangeData> r00_newestSortKey(
+ @Named("A") SortKeyPredicate.After a, @Named("B") SortKeyPredicate.After b) {
+ return a.getValue().compareTo(b.getValue()) >= 0 ? a : b;
+ }
+
+ private static final class InvalidProvider<T> implements Provider<T> {
+ @Override
+ public T get() {
+ throw new OutOfScopeException("Not available at init");
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
deleted file mode 100644
index e5b4143..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
-
-class BranchPredicate extends OperatorPredicate<ChangeData> {
- private final Provider<ReviewDb> dbProvider;
-
- BranchPredicate(Provider<ReviewDb> dbProvider, String branch) {
- super(ChangeQueryBuilder.FIELD_BRANCH, branch.startsWith(Branch.R_HEADS)
- ? branch : Branch.R_HEADS + branch);
- this.dbProvider = dbProvider;
- }
-
- @Override
- public boolean match(final ChangeData object) throws OrmException {
- Change change = object.change(dbProvider);
- if (change == null) {
- return false;
- }
- return change.getDest().get().startsWith(Branch.R_HEADS)
- && getValue().equals(change.getDest().get());
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 506fabc..4522fe6 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
@@ -51,7 +51,6 @@
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -72,8 +71,8 @@
return SORT_APPROVALS.sortedCopy(approvals);
}
- public static void ensureChangeLoaded(
- Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+ public static void ensureChangeLoaded(Provider<ReviewDb> db,
+ Iterable<ChangeData> changes) throws OrmException {
Map<Change.Id, ChangeData> missing = Maps.newHashMap();
for (ChangeData cd : changes) {
if (cd.change == null) {
@@ -87,8 +86,15 @@
}
}
- public static void ensureCurrentPatchSetLoaded(
- Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+ public static void ensureAllPatchSetsLoaded(Provider<ReviewDb> db,
+ Iterable<ChangeData> changes) throws OrmException {
+ for (ChangeData cd : changes) {
+ cd.patches(db);
+ }
+ }
+
+ public static void ensureCurrentPatchSetLoaded(Provider<ReviewDb> db,
+ Iterable<ChangeData> changes) throws OrmException {
Map<PatchSet.Id, ChangeData> missing = Maps.newHashMap();
for (ChangeData cd : changes) {
if (cd.currentPatchSet == null && cd.patches == null) {
@@ -106,8 +112,8 @@
}
}
- public static void ensureCurrentApprovalsLoaded(
- Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+ public static void ensureCurrentApprovalsLoaded(Provider<ReviewDb> db,
+ Iterable<ChangeData> changes) throws OrmException {
List<ResultSet<PatchSetApproval>> pending = Lists.newArrayList();
for (ChangeData cd : changes) {
if (cd.currentApprovals == null && cd.limitedApprovals == null) {
@@ -126,6 +132,7 @@
}
private final Change.Id legacyId;
+ private ChangeDataSource returnedBySource;
private Change change;
private String commitMessage;
private PatchSet currentPatchSet;
@@ -134,13 +141,14 @@
private ListMultimap<PatchSet.Id, PatchSetApproval> limitedApprovals;
private ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals;
private List<PatchSetApproval> currentApprovals;
- private String[] currentFiles;
+ private List<String> currentFiles;
private Collection<PatchLineComment> comments;
private Collection<TrackingId> trackingIds;
private CurrentUser visibleTo;
private ChangeControl changeControl;
private List<ChangeMessage> messages;
private List<SubmitRecord> submitRecords;
+ private boolean patchesLoaded;
public ChangeData(final Change.Id id) {
legacyId = id;
@@ -157,6 +165,14 @@
changeControl = c;
}
+ public boolean isFromSource(ChangeDataSource s) {
+ return s == returnedBySource;
+ }
+
+ public void cacheFromSource(ChangeDataSource s) {
+ returnedBySource = s;
+ }
+
public void limitToPatchSets(Collection<PatchSet.Id> ids) {
limitedIds = Sets.newLinkedHashSetWithExpectedSize(ids.size());
for (PatchSet.Id id : ids) {
@@ -172,11 +188,11 @@
return limitedIds;
}
- public void setCurrentFilePaths(String[] filePaths) {
+ public void setCurrentFilePaths(List<String> filePaths) {
currentFiles = filePaths;
}
- public String[] currentFilePaths(Provider<ReviewDb> db,
+ public List<String> currentFilePaths(Provider<ReviewDb> db,
PatchListCache cache) throws OrmException {
if (currentFiles == null) {
Change c = change(db);
@@ -192,7 +208,7 @@
try {
p = cache.get(c, ps);
} catch (PatchListNotAvailableException e) {
- currentFiles = new String[0];
+ currentFiles = Collections.emptyList();
return currentFiles;
}
@@ -218,8 +234,8 @@
break;
}
}
- currentFiles = r.toArray(new String[r.size()]);
- Arrays.sort(currentFiles);
+ Collections.sort(r);
+ currentFiles = Collections.unmodifiableList(r);
}
return currentFiles;
}
@@ -256,6 +272,10 @@
return change;
}
+ void setChange(Change c) {
+ change = c;
+ }
+
public PatchSet currentPatchSet(Provider<ReviewDb> db) throws OrmException {
if (currentPatchSet == null) {
Change c = change(db);
@@ -291,6 +311,10 @@
return currentApprovals;
}
+ public void setCurrentApprovals(List<PatchSetApproval> approvals) {
+ currentApprovals = approvals;
+ }
+
public String commitMessage(GitRepositoryManager repoManager,
Provider<ReviewDb> db) throws IOException, OrmException {
if (commitMessage == null) {
@@ -321,7 +345,7 @@
*/
public Collection<PatchSet> patches(Provider<ReviewDb> db)
throws OrmException {
- if (patches == null) {
+ if (patches == null || !patchesLoaded) {
if (limitedIds != null) {
patches = Lists.newArrayList();
for (PatchSet ps : db.get().patchSets().byChange(legacyId)) {
@@ -332,6 +356,7 @@
} else {
patches = db.get().patchSets().byChange(legacyId).toList();
}
+ patchesLoaded = true;
}
return patches;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
index 7116aa9..457d657 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
@@ -16,17 +16,18 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Provider;
-class ChangeIdPredicate extends OperatorPredicate<ChangeData> implements
+class ChangeIdPredicate extends IndexPredicate<ChangeData> implements
ChangeDataSource {
private final Provider<ReviewDb> dbProvider;
ChangeIdPredicate(Provider<ReviewDb> dbProvider, String id) {
- super(ChangeQueryBuilder.FIELD_CHANGE, id);
+ super(ChangeField.ID, ChangeQueryBuilder.FIELD_CHANGE, id);
this.dbProvider = dbProvider;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index e74172e..7684484 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -14,10 +14,12 @@
package com.google.gerrit.server.query.change;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
@@ -30,6 +32,8 @@
import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexCollection;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectCache;
@@ -49,6 +53,7 @@
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
@@ -76,6 +81,7 @@
public static final String FIELD_AGE = "age";
public static final String FIELD_BRANCH = "branch";
public static final String FIELD_CHANGE = "change";
+ public static final String FIELD_COMMENT = "comment";
public static final String FIELD_COMMIT = "commit";
public static final String FIELD_DRAFTBY = "draftby";
public static final String FIELD_FILE = "file";
@@ -97,11 +103,31 @@
public static final String FIELD_VISIBLETO = "visibleto";
public static final String FIELD_WATCHEDBY = "watchedby";
+ public static final String ARG_ID_USER = "user";
+ public static final String ARG_ID_GROUP = "group";
+
+
private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef =
new QueryBuilder.Definition<ChangeData, ChangeQueryBuilder>(
ChangeQueryBuilder.class);
- static class Arguments {
+ @SuppressWarnings("unchecked")
+ public static boolean hasLimit(Predicate<ChangeData> p) {
+ return find(p, IntPredicate.class, FIELD_LIMIT) != null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static int getLimit(Predicate<ChangeData> p) {
+ return ((IntPredicate<?>) find(p, IntPredicate.class, FIELD_LIMIT)).intValue();
+ }
+
+ public static boolean hasSortKey(Predicate<ChangeData> p) {
+ return find(p, SortKeyPredicate.class, "sortkey_after") != null
+ || find(p, SortKeyPredicate.class, "sortkey_before") != null;
+ }
+
+ @VisibleForTesting
+ public static class Arguments {
final Provider<ReviewDb> dbProvider;
final Provider<ChangeQueryRewriter> rewriter;
final IdentifiedUser.GenericFactory userFactory;
@@ -113,9 +139,11 @@
final PatchListCache patchListCache;
final GitRepositoryManager repoManager;
final ProjectCache projectCache;
+ final IndexCollection indexes;
@Inject
- Arguments(Provider<ReviewDb> dbProvider,
+ @VisibleForTesting
+ public Arguments(Provider<ReviewDb> dbProvider,
Provider<ChangeQueryRewriter> rewriter,
IdentifiedUser.GenericFactory userFactory,
CapabilityControl.Factory capabilityControlFactory,
@@ -125,7 +153,8 @@
AllProjectsName allProjectsName,
PatchListCache patchListCache,
GitRepositoryManager repoManager,
- ProjectCache projectCache) {
+ ProjectCache projectCache,
+ IndexCollection indexes) {
this.dbProvider = dbProvider;
this.rewriter = rewriter;
this.userFactory = userFactory;
@@ -137,6 +166,7 @@
this.patchListCache = patchListCache;
this.repoManager = repoManager;
this.projectCache = projectCache;
+ this.indexes = indexes;
}
}
@@ -146,17 +176,26 @@
private final Arguments args;
private final CurrentUser currentUser;
- private boolean allowsFile;
+ private boolean allowFileRegex;
@Inject
- ChangeQueryBuilder(Arguments args, @Assisted CurrentUser currentUser) {
+ public ChangeQueryBuilder(Arguments args, @Assisted CurrentUser currentUser) {
super(mydef);
this.args = args;
this.currentUser = currentUser;
}
- public void setAllowFile(boolean on) {
- allowsFile = on;
+ @VisibleForTesting
+ protected ChangeQueryBuilder(
+ QueryBuilder.Definition<ChangeData, ? extends ChangeQueryBuilder> def,
+ Arguments args, CurrentUser currentUser) {
+ super(def);
+ this.args = args;
+ this.currentUser = currentUser;
+ }
+
+ public void setAllowFileRegex(boolean on) {
+ allowFileRegex = on;
}
@Operator
@@ -181,6 +220,15 @@
}
@Operator
+ public Predicate<ChangeData> comment(String value) throws QueryParseException {
+ ChangeIndex index = args.indexes.getSearchIndex();
+ if (index == null) {
+ throw error("secondary index must be enabled for comment:" + value);
+ }
+ return new CommentPredicate(args.dbProvider, index, value);
+ }
+
+ @Operator
public Predicate<ChangeData> status(String statusName) {
if ("open".equals(statusName)) {
return status_open();
@@ -220,7 +268,7 @@
}
if ("watched".equalsIgnoreCase(value)) {
- return new IsWatchedByPredicate(args, currentUser);
+ return new IsWatchedByPredicate(args, currentUser, false);
}
if ("visible".equalsIgnoreCase(value)) {
@@ -264,8 +312,14 @@
@Operator
public Predicate<ChangeData> branch(String name) {
if (name.startsWith("^"))
- return new RegexBranchPredicate(args.dbProvider, name);
- return new BranchPredicate(args.dbProvider, name);
+ return ref("^" + branchToRef(name.substring(1)));
+ return ref(branchToRef(name));
+ }
+
+ private static String branchToRef(String name) {
+ if (!name.startsWith(Branch.R_HEADS))
+ return Branch.R_HEADS + name;
+ return name;
}
@Operator
@@ -284,27 +338,82 @@
@Operator
public Predicate<ChangeData> file(String file) throws QueryParseException {
- if (!allowsFile) {
- throw error("operator not permitted here: file:" + file);
- }
-
if (file.startsWith("^")) {
- return new RegexFilePredicate(args.dbProvider, args.patchListCache, file);
+ if (allowFileRegex || args.indexes.getSearchIndex() != null) {
+ return new RegexFilePredicate(args.dbProvider, args.patchListCache, file);
+ } else {
+ throw error("secondary index must be enabled for file:" + file);
+ }
+ } else {
+ if (args.indexes.getSearchIndex() == null) {
+ throw error("secondary index must be enabled for file:" + file);
+ }
+ return new EqualsFilePredicate(args.dbProvider, args.patchListCache, file);
}
-
- throw new IllegalArgumentException();
}
@Operator
- public Predicate<ChangeData> label(String name) {
+ public Predicate<ChangeData> label(String name) throws QueryParseException,
+ OrmException {
+ Set<Account.Id> accounts = null;
+ AccountGroup.UUID group = null;
+
+ // Parse for:
+ // label:CodeReview=1,user=jsmith or
+ // label:CodeReview=1,jsmith or
+ // label:CodeReview=1,group=android_approvers or
+ // label:CodeReview=1,android_approvers
+ // user/groups without a label will first attempt to match user
+ String[] splitReviewer = name.split(",", 2);
+ name = splitReviewer[0]; // remove all but the vote piece, e.g.'CodeReview=1'
+
+ if (splitReviewer.length == 2) {
+ // process the user/group piece
+ PredicateArgs lblArgs = new PredicateArgs(splitReviewer[1]);
+
+ for (Map.Entry<String, String> pair : lblArgs.keyValue.entrySet()) {
+ if (pair.getKey().equalsIgnoreCase(ARG_ID_USER)) {
+ accounts = parseAccount(pair.getValue());
+ } else if (pair.getKey().equalsIgnoreCase(ARG_ID_GROUP)) {
+ group = parseGroup(pair.getValue()).getUUID();
+ } else {
+ throw new QueryParseException(
+ "Invalid argument identifier '" + pair.getKey() + "'");
+ }
+ }
+
+ for (String value : lblArgs.positional) {
+ if (accounts != null || group != null) {
+ throw new QueryParseException("more than one user/group specified (" +
+ value + ")");
+ }
+ try {
+ accounts = parseAccount(value);
+ } catch (QueryParseException qpex) {
+ // If it doesn't match an account, see if it matches a group
+ // (accounts get precedence)
+ try {
+ group = parseGroup(value).getUUID();
+ } catch (QueryParseException e) {
+ throw error("Neither user nor group " + value + " found");
+ }
+ }
+ }
+ }
+
return new LabelPredicate(args.projectCache,
args.changeControlGenericFactory, args.userFactory, args.dbProvider,
- name);
+ name, accounts, group);
}
@Operator
- public Predicate<ChangeData> message(String text) {
- return new MessagePredicate(args.dbProvider, args.repoManager, text);
+ public Predicate<ChangeData> message(String text) throws QueryParseException {
+ ChangeIndex index = args.indexes.getSearchIndex();
+ if (index == null) {
+ return new LegacyMessagePredicate(args.dbProvider, args.repoManager, text);
+ }
+
+ return new MessagePredicate(args.dbProvider, index, text);
}
@Operator
@@ -328,12 +437,12 @@
Set<Account.Id> m = parseAccount(who);
List<IsWatchedByPredicate> p = Lists.newArrayListWithCapacity(m.size());
for (Account.Id id : m) {
- if (currentUser instanceof IdentifiedUser
+ if (currentUser.isIdentifiedUser()
&& id.equals(((IdentifiedUser) currentUser).getAccountId())) {
- p.add(new IsWatchedByPredicate(args, currentUser));
+ p.add(new IsWatchedByPredicate(args, currentUser, false));
} else {
p.add(new IsWatchedByPredicate(args,
- args.userFactory.create(args.dbProvider, id)));
+ args.userFactory.create(args.dbProvider, id), true));
}
}
return Predicate.or(p);
@@ -476,21 +585,6 @@
}
@SuppressWarnings("unchecked")
- public boolean hasLimit(Predicate<ChangeData> p) {
- return find(p, IntPredicate.class, FIELD_LIMIT) != null;
- }
-
- @SuppressWarnings("unchecked")
- public int getLimit(Predicate<ChangeData> p) {
- return ((IntPredicate<?>) find(p, IntPredicate.class, FIELD_LIMIT)).intValue();
- }
-
- public boolean hasSortKey(Predicate<ChangeData> p) {
- return find(p, SortKeyPredicate.class, "sortkey_after") != null
- || find(p, SortKeyPredicate.class, "sortkey_before") != null;
- }
-
- @SuppressWarnings("unchecked")
@Override
protected Predicate<ChangeData> defaultField(String query)
throws QueryParseException {
@@ -511,7 +605,11 @@
}
} else if (PAT_LABEL.matcher(query).find()) {
- return label(query);
+ try {
+ return label(query);
+ } catch (OrmException err) {
+ throw error("Cannot lookup user", err);
+ }
} else {
// Try to match a project name by substring query.
@@ -548,8 +646,17 @@
return matches;
}
+ private GroupReference parseGroup(String group) throws QueryParseException {
+ GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend,
+ group);
+ if (g == null) {
+ throw error("Group " + group + " not found");
+ }
+ return g;
+ }
+
private Account.Id self() {
- if (currentUser instanceof IdentifiedUser) {
+ if (currentUser.isIdentifiedUser()) {
return ((IdentifiedUser) currentUser).getAccountId();
}
throw new IllegalArgumentException();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
index e6251bc..bd186c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,693 +14,10 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ChangeAccess;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.query.IntPredicate;
import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryRewriter;
-import com.google.gerrit.server.query.RewritePredicate;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import com.google.inject.Inject;
-import com.google.inject.OutOfScopeException;
-import com.google.inject.Provider;
-import com.google.inject.name.Named;
+import com.google.gerrit.server.query.QueryParseException;
-import java.util.Collection;
-
-public class ChangeQueryRewriter extends QueryRewriter<ChangeData> {
- private static final QueryRewriter.Definition<ChangeData, ChangeQueryRewriter> mydef =
- new QueryRewriter.Definition<ChangeData, ChangeQueryRewriter>(
- ChangeQueryRewriter.class, new ChangeQueryBuilder(
- new ChangeQueryBuilder.Arguments( //
- new InvalidProvider<ReviewDb>(), //
- new InvalidProvider<ChangeQueryRewriter>(), //
- null, null, null, null, null, //
- null, null, null, null), null));
-
- private final Provider<ReviewDb> dbProvider;
-
- @Inject
- ChangeQueryRewriter(Provider<ReviewDb> dbProvider) {
- super(mydef);
- this.dbProvider = dbProvider;
- }
-
- @Override
- public Predicate<ChangeData> and(Collection<? extends Predicate<ChangeData>> l) {
- return hasSource(l) ? new AndSource(dbProvider, l) : super.and(l);
- }
-
- @Override
- public Predicate<ChangeData> or(Collection<? extends Predicate<ChangeData>> l) {
- return hasSource(l) ? new OrSource(l) : super.or(l);
- }
-
- @Rewrite("-status:open")
- @NoCostComputation
- public Predicate<ChangeData> r00_notOpen() {
- return ChangeStatusPredicate.closed(dbProvider);
- }
-
- @Rewrite("-status:closed")
- @NoCostComputation
- public Predicate<ChangeData> r00_notClosed() {
- return ChangeStatusPredicate.open(dbProvider);
- }
-
- @SuppressWarnings("unchecked")
- @NoCostComputation
- @Rewrite("-status:merged")
- public Predicate<ChangeData> r00_notMerged() {
- return or(ChangeStatusPredicate.open(dbProvider),
- new ChangeStatusPredicate(dbProvider, Change.Status.ABANDONED));
- }
-
- @SuppressWarnings("unchecked")
- @NoCostComputation
- @Rewrite("-status:abandoned")
- public Predicate<ChangeData> r00_notAbandoned() {
- return or(ChangeStatusPredicate.open(dbProvider),
- new ChangeStatusPredicate(dbProvider, Change.Status.MERGED));
- }
-
- @SuppressWarnings("unchecked")
- @NoCostComputation
- @Rewrite("sortkey_before:z A=(age:*)")
- public Predicate<ChangeData> r00_ageToSortKey(@Named("A") AgePredicate a) {
- String cut = ChangeUtil.sortKey(a.getCut(), Integer.MAX_VALUE);
- return and(new SortKeyPredicate.Before(dbProvider, cut), a);
- }
-
- @NoCostComputation
- @Rewrite("A=(limit:*) B=(limit:*)")
- public Predicate<ChangeData> r00_smallestLimit(
- @Named("A") IntPredicate<ChangeData> a,
- @Named("B") IntPredicate<ChangeData> b) {
- return a.intValue() <= b.intValue() ? a : b;
- }
-
- @NoCostComputation
- @Rewrite("A=(sortkey_before:*) B=(sortkey_before:*)")
- public Predicate<ChangeData> r00_oldestSortKey(
- @Named("A") SortKeyPredicate.Before a,
- @Named("B") SortKeyPredicate.Before b) {
- return a.getValue().compareTo(b.getValue()) <= 0 ? a : b;
- }
-
- @NoCostComputation
- @Rewrite("A=(sortkey_after:*) B=(sortkey_after:*)")
- public Predicate<ChangeData> r00_newestSortKey(
- @Named("A") SortKeyPredicate.After a, @Named("B") SortKeyPredicate.After b) {
- return a.getValue().compareTo(b.getValue()) >= 0 ? a : b;
- }
-
- @Rewrite("status:open P=(project:*) B=(branch:*)")
- public Predicate<ChangeData> r05_byBranchOpen(
- @Named("P") final ProjectPredicate p,
- @Named("B") final BranchPredicate b) {
- return new ChangeSource(500) {
- @Override
- ResultSet<Change> scan(ChangeAccess a)
- throws OrmException {
- return a.byBranchOpenAll(
- new Branch.NameKey(p.getValueKey(), b.getValue()));
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isOpen()
- && p.match(cd)
- && b.match(cd);
- }
- };
- }
-
- @Rewrite("status:merged P=(project:*) B=(branch:*) S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r05_byBranchMergedPrev(
- @Named("P") final ProjectPredicate p,
- @Named("B") final BranchPredicate b,
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(40000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byBranchClosedPrev(Change.Status.MERGED.getCode(), //
- new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.MERGED
- && p.match(cd) //
- && b.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:merged P=(project:*) B=(branch:*) S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r05_byBranchMergedNext(
- @Named("P") final ProjectPredicate p,
- @Named("B") final BranchPredicate b,
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(40000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byBranchClosedNext(Change.Status.MERGED.getCode(), //
- new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.MERGED
- && p.match(cd) //
- && b.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:open P=(project:*) S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r10_byProjectOpenPrev(
- @Named("P") final ProjectPredicate p,
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(500, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byProjectOpenPrev(p.getValueKey(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isOpen() //
- && p.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:open P=(project:*) S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r10_byProjectOpenNext(
- @Named("P") final ProjectPredicate p,
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(500, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byProjectOpenNext(p.getValueKey(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isOpen() //
- && p.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:merged P=(project:*) S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r10_byProjectMergedPrev(
- @Named("P") final ProjectPredicate p,
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(40000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byProjectClosedPrev(Change.Status.MERGED.getCode(), //
- p.getValueKey(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.MERGED
- && p.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:merged P=(project:*) S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r10_byProjectMergedNext(
- @Named("P") final ProjectPredicate p,
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(40000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byProjectClosedNext(Change.Status.MERGED.getCode(), //
- p.getValueKey(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.MERGED
- && p.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:abandoned P=(project:*) S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r10_byProjectAbandonedPrev(
- @Named("P") final ProjectPredicate p,
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(40000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byProjectClosedPrev(Change.Status.ABANDONED.getCode(), //
- p.getValueKey(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
- && p.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:abandoned P=(project:*) S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r10_byProjectAbandonedNext(
- @Named("P") final ProjectPredicate p,
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(40000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.byProjectClosedNext(Change.Status.ABANDONED.getCode(), //
- p.getValueKey(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
- && p.match(cd) //
- && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:open S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byOpenPrev(
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(2000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.allOpenPrev(key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
- }
- };
- }
-
- @Rewrite("status:open S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byOpenNext(
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(2000, s.getValue(), l.intValue()) {
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.allOpenNext(key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:merged S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byMergedPrev(
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(50000, s.getValue(), l.intValue()) {
- {
- init("r20_byMergedPrev", s, l);
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.allClosedPrev(Change.Status.MERGED.getCode(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.MERGED
- && s.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:merged S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byMergedNext(
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(50000, s.getValue(), l.intValue()) {
- {
- init("r20_byMergedNext", s, l);
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.allClosedNext(Change.Status.MERGED.getCode(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.MERGED
- && s.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:abandoned S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byAbandonedPrev(
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(50000, s.getValue(), l.intValue()) {
- {
- init("r20_byAbandonedPrev", s, l);
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.allClosedPrev(Change.Status.ABANDONED.getCode(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
- && s.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:abandoned S=(sortkey_before:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byAbandonedNext(
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return new PaginatedSource(50000, s.getValue(), l.intValue()) {
- {
- init("r20_byAbandonedNext", s, l);
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException {
- return a.allClosedNext(Change.Status.ABANDONED.getCode(), key, limit);
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
- && s.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byClosedPrev(
- @Named("S") final SortKeyPredicate.After s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return or(r20_byMergedPrev(s, l), r20_byAbandonedPrev(s, l));
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
- public Predicate<ChangeData> r20_byClosedNext(
- @Named("S") final SortKeyPredicate.Before s,
- @Named("L") final IntPredicate<ChangeData> l) {
- return or(r20_byMergedNext(s, l), r20_byAbandonedNext(s, l));
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:open O=(owner:*)")
- public Predicate<ChangeData> r25_byOwnerOpen(
- @Named("O") final OwnerPredicate o) {
- return new ChangeSource(50) {
- {
- init("r25_byOwnerOpen", o);
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a) throws OrmException {
- return a.byOwnerOpen(o.getAccountId());
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isOpen() && o.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:closed O=(owner:*)")
- public Predicate<ChangeData> r25_byOwnerClosed(
- @Named("O") final OwnerPredicate o) {
- return new ChangeSource(5000) {
- {
- init("r25_byOwnerClosed", o);
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a) throws OrmException {
- return a.byOwnerClosedAll(o.getAccountId());
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus().isClosed() && o.match(cd);
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("O=(owner:*)")
- public Predicate<ChangeData> r26_byOwner(@Named("O") OwnerPredicate o) {
- return or(r25_byOwnerOpen(o), r25_byOwnerClosed(o));
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:open R=(reviewer:*)")
- public Predicate<ChangeData> r30_byReviewerOpen(
- @Named("R") final ReviewerPredicate r) {
- return new Source() {
- {
- init("r30_byReviewerOpen", r);
- }
-
- @Override
- public ResultSet<ChangeData> read() throws OrmException {
- return ChangeDataResultSet.patchSetApproval(dbProvider.get()
- .patchSetApprovals().openByUser(r.getAccountId()));
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- Change change = cd.change(dbProvider);
- return change != null && change.getStatus().isOpen() && r.match(cd);
- }
-
- @Override
- public int getCardinality() {
- return 50;
- }
-
- @Override
- public int getCost() {
- return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("status:closed R=(reviewer:*)")
- public Predicate<ChangeData> r30_byReviewerClosed(
- @Named("R") final ReviewerPredicate r) {
- return new Source() {
- {
- init("r30_byReviewerClosed", r);
- }
-
- @Override
- public ResultSet<ChangeData> read() throws OrmException {
- return ChangeDataResultSet.patchSetApproval(dbProvider.get()
- .patchSetApprovals().closedByUserAll(r.getAccountId()));
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- Change change = cd.change(dbProvider);
- return change != null && change.getStatus().isClosed() && r.match(cd);
- }
-
- @Override
- public int getCardinality() {
- return 5000;
- }
-
- @Override
- public int getCost() {
- return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
- }
- };
- }
-
- @SuppressWarnings("unchecked")
- @Rewrite("R=(reviewer:*)")
- public Predicate<ChangeData> r31_byReviewer(
- @Named("R") final ReviewerPredicate r) {
- return or(r30_byReviewerOpen(r), r30_byReviewerClosed(r));
- }
-
- @Rewrite("status:submitted")
- public Predicate<ChangeData> r99_allSubmitted() {
- return new ChangeSource(50) {
- @Override
- ResultSet<Change> scan(ChangeAccess a) throws OrmException {
- return a.allSubmitted();
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return cd.change(dbProvider).getStatus() == Change.Status.SUBMITTED;
- }
- };
- }
-
- @Rewrite("P=(project:*)")
- public Predicate<ChangeData> r99_byProject(
- @Named("P") final ProjectPredicate p) {
- return new ChangeSource(1000000) {
- @Override
- ResultSet<Change> scan(ChangeAccess a) throws OrmException {
- return a.byProject(p.getValueKey());
- }
-
- @Override
- public boolean match(ChangeData cd) throws OrmException {
- return p.match(cd);
- }
- };
- }
-
- private static boolean hasSource(Collection<? extends Predicate<ChangeData>> l) {
- for (Predicate<ChangeData> p : l) {
- if (p instanceof ChangeDataSource) {
- return true;
- }
- }
- return false;
- }
-
- private abstract static class Source extends RewritePredicate<ChangeData>
- implements ChangeDataSource {
- @Override
- public boolean hasChange() {
- return false;
- }
- }
-
- private abstract class ChangeSource extends Source {
- private final int cardinality;
-
- ChangeSource(int card) {
- this.cardinality = card;
- }
-
- abstract ResultSet<Change> scan(ChangeAccess a) throws OrmException;
-
- @Override
- public ResultSet<ChangeData> read() throws OrmException {
- return ChangeDataResultSet.change(scan(dbProvider.get().changes()));
- }
-
- @Override
- public boolean hasChange() {
- return true;
- }
-
- @Override
- public int getCardinality() {
- return cardinality;
- }
-
- @Override
- public int getCost() {
- return ChangeCosts.cost(ChangeCosts.CHANGES_SCAN, getCardinality());
- }
- }
-
- private abstract class PaginatedSource extends ChangeSource implements
- Paginated {
- private final String startKey;
- private final int limit;
-
- PaginatedSource(int card, String start, int lim) {
- super(card);
- this.startKey = start;
- this.limit = lim;
- }
-
- @Override
- public int limit() {
- return limit;
- }
-
- @Override
- ResultSet<Change> scan(ChangeAccess a) throws OrmException {
- return scan(a, startKey, limit);
- }
-
- @Override
- public ResultSet<ChangeData> restart(ChangeData last) throws OrmException {
- return ChangeDataResultSet.change(scan(dbProvider.get().changes(), //
- last.change(dbProvider).getSortKey(), //
- limit));
- }
-
- abstract ResultSet<Change> scan(ChangeAccess a, String key, int limit)
- throws OrmException;
- }
-
- private static final class InvalidProvider<T> implements Provider<T> {
- @Override
- public T get() {
- throw new OutOfScopeException("Not available at init");
- }
- }
+public interface ChangeQueryRewriter {
+ Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
+ throws QueryParseException;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 6e9e79c..d5d6a92 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -14,19 +14,19 @@
package com.google.gerrit.server.query.change;
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableBiMap;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
import com.google.gerrit.server.query.Predicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
import java.util.ArrayList;
-import java.util.EnumMap;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
-
/**
* Predicate for a {@link Change.Status}.
@@ -35,21 +35,19 @@
* status:} but may also be {@code is:} to help do-what-i-meanery for end-users
* searching for changes. Either operator name has the same meaning.
*/
-final class ChangeStatusPredicate extends OperatorPredicate<ChangeData> {
- private static final Map<String, Change.Status> byName;
- private static final EnumMap<Change.Status, String> byEnum;
+public final class ChangeStatusPredicate extends IndexPredicate<ChangeData> {
+ public static final ImmutableBiMap<Change.Status, String> VALUES;
static {
- byName = new HashMap<String, Change.Status>();
- byEnum = new EnumMap<Change.Status, String>(Change.Status.class);
- for (final Change.Status s : Change.Status.values()) {
- final String name = s.name().toLowerCase();
- byName.put(name, s);
- byEnum.put(s, name);
+ ImmutableBiMap.Builder<Change.Status, String> values =
+ ImmutableBiMap.builder();
+ for (Change.Status s : Change.Status.values()) {
+ values.put(s, s.name().toLowerCase());
}
+ VALUES = values.build();
}
- static Predicate<ChangeData> open(Provider<ReviewDb> dbProvider) {
+ public static Predicate<ChangeData> open(Provider<ReviewDb> dbProvider) {
List<Predicate<ChangeData>> r = new ArrayList<Predicate<ChangeData>>(4);
for (final Change.Status e : Change.Status.values()) {
if (e.isOpen()) {
@@ -59,7 +57,7 @@
return r.size() == 1 ? r.get(0) : or(r);
}
- static Predicate<ChangeData> closed(Provider<ReviewDb> dbProvider) {
+ public static Predicate<ChangeData> closed(Provider<ReviewDb> dbProvider) {
List<Predicate<ChangeData>> r = new ArrayList<Predicate<ChangeData>>(4);
for (final Change.Status e : Change.Status.values()) {
if (e.isClosed()) {
@@ -69,28 +67,23 @@
return r.size() == 1 ? r.get(0) : or(r);
}
- private static Change.Status parse(final String value) {
- final Change.Status s = byName.get(value);
- if (s == null) {
- throw new IllegalArgumentException();
- }
- return s;
- }
-
private final Provider<ReviewDb> dbProvider;
private final Change.Status status;
ChangeStatusPredicate(Provider<ReviewDb> dbProvider, String value) {
- this(dbProvider, parse(value));
+ super(ChangeField.STATUS, value);
+ this.dbProvider = dbProvider;
+ status = VALUES.inverse().get(value);
+ checkArgument(status != null, "invalid change status: %s", value);
}
ChangeStatusPredicate(Provider<ReviewDb> dbProvider, Change.Status status) {
- super(ChangeQueryBuilder.FIELD_STATUS, byEnum.get(status));
+ super(ChangeField.STATUS, VALUES.get(status));
this.dbProvider = dbProvider;
this.status = status;
}
- Change.Status getStatus() {
+ public Change.Status getStatus() {
return status;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
new file mode 100644
index 0000000..563e37b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+
+class CommentPredicate extends IndexPredicate<ChangeData> {
+ private final Provider<ReviewDb> db;
+ private final ChangeIndex index;
+
+ CommentPredicate(Provider<ReviewDb> db, ChangeIndex index, String value) {
+ super(ChangeField.COMMENT, value);
+ this.db = db;
+ this.index = index;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean match(ChangeData object) throws OrmException {
+ try {
+ for (ChangeData cData : index.getSource(
+ Predicate.and(new LegacyChangeIdPredicate(db, object.getId()), this), 1)
+ .read()) {
+ if (cData.getId().equals(object.getId())) {
+ return true;
+ }
+ }
+ } catch (QueryParseException e) {
+ throw new OrmException(e);
+ }
+
+ return false;
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
index 274b40c..082dc83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -17,7 +17,8 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.ObjectIdPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Provider;
@@ -25,13 +26,15 @@
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.ObjectId;
-class CommitPredicate extends ObjectIdPredicate<ChangeData> implements
+class CommitPredicate extends IndexPredicate<ChangeData> implements
ChangeDataSource {
private final Provider<ReviewDb> dbProvider;
+ private final AbbreviatedObjectId abbrevId;
CommitPredicate(Provider<ReviewDb> dbProvider, AbbreviatedObjectId id) {
- super(ChangeQueryBuilder.FIELD_COMMIT, id);
+ super(ChangeField.COMMIT, id.name());
this.dbProvider = dbProvider;
+ this.abbrevId = id;
}
@Override
@@ -39,7 +42,7 @@
for (PatchSet p : object.patches(dbProvider)) {
if (p.getRevision() != null && p.getRevision().get() != null) {
final ObjectId id = ObjectId.fromString(p.getRevision().get());
- if (abbreviated().prefixCompare(id) == 0) {
+ if (abbrevId.prefixCompare(id) == 0) {
return true;
}
}
@@ -49,7 +52,7 @@
@Override
public ResultSet<ChangeData> read() throws OrmException {
- final RevId id = new RevId(abbreviated().name());
+ final RevId id = new RevId(abbrevId.name());
if (id.isComplete()) {
return ChangeDataResultSet.patchSet(//
dbProvider.get().patchSets().byRevision(id));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
new file mode 100644
index 0000000..002dc99
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+import java.util.List;
+
+class EqualsFilePredicate extends IndexPredicate<ChangeData> {
+ private final Provider<ReviewDb> db;
+ private final PatchListCache cache;
+ private final String value;
+
+ EqualsFilePredicate(Provider<ReviewDb> db, PatchListCache plc, String value) {
+ super(ChangeField.FILE, value);
+ this.db = db;
+ this.cache = plc;
+ this.value = value;
+ }
+
+ @Override
+ public boolean match(ChangeData object) throws OrmException {
+ List<String> files = object.currentFilePaths(db, cache);
+ if (files != null) {
+ return Collections.binarySearch(files, value) >= 0;
+ } else {
+ // The ChangeData can't do expensive lookups right now. Bypass
+ // them and include the result anyway. We might be able to do
+ // a narrow later on to a smaller set.
+ //
+ return true;
+ }
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
new file mode 100644
index 0000000..af9a07a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -0,0 +1,153 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+
+class EqualsLabelPredicate extends IndexPredicate<ChangeData> {
+ private final ProjectCache projectCache;
+ private final ChangeControl.GenericFactory ccFactory;
+ private final IdentifiedUser.GenericFactory userFactory;
+ private final Provider<ReviewDb> dbProvider;
+ private final String label;
+ private final int expVal;
+ private final Account.Id account;
+ private final AccountGroup.UUID group;
+
+ EqualsLabelPredicate(ProjectCache projectCache,
+ ChangeControl.GenericFactory ccFactory,
+ IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+ String label, int expVal, Account.Id account,
+ AccountGroup.UUID group) {
+ super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account));
+ this.ccFactory = ccFactory;
+ this.projectCache = projectCache;
+ this.userFactory = userFactory;
+ this.dbProvider = dbProvider;
+ this.label = label;
+ this.expVal = expVal;
+ this.account = account;
+ this.group = group;
+ }
+
+ @Override
+ public boolean match(ChangeData object) throws OrmException {
+ Change c = object.change(dbProvider);
+ if (c == null) {
+ // The change has disappeared.
+ //
+ return false;
+ }
+ ProjectState project = projectCache.get(c.getDest().getParentKey());
+ if (project == null) {
+ // The project has disappeared.
+ //
+ return false;
+ }
+ LabelType labelType = type(project.getLabelTypes(), label);
+ boolean hasVote = false;
+ for (PatchSetApproval p : object.currentApprovals(dbProvider)) {
+ if (labelType.matches(p)) {
+ hasVote = true;
+ if (match(c, p.getValue(), p.getAccountId(), labelType)) {
+ return true;
+ }
+ }
+ }
+
+ if (!hasVote && expVal == 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static LabelType type(LabelTypes types, String toFind) {
+ if (types.byLabel(toFind) != null) {
+ return types.byLabel(toFind);
+ }
+
+ for (LabelType lt : types.getLabelTypes()) {
+ if (toFind.equalsIgnoreCase(lt.getName())) {
+ return lt;
+ }
+ }
+
+ for (LabelType lt : types.getLabelTypes()) {
+ if (toFind.equalsIgnoreCase(lt.getAbbreviation())) {
+ return lt;
+ }
+ }
+
+ return LabelType.withDefaultValues(toFind);
+ }
+
+ private boolean match(Change change, int value, Account.Id approver,
+ LabelType type) throws OrmException {
+ int psVal = value;
+ if (psVal == expVal) {
+ // Double check the value is still permitted for the user.
+ //
+ IdentifiedUser reviewer = userFactory.create(dbProvider, approver);
+ try {
+ ChangeControl cc = ccFactory.controlFor(change, reviewer);
+ if (!cc.isVisible(dbProvider.get())) {
+ // The user can't see the change anymore.
+ //
+ return false;
+ }
+ psVal = cc.getRange(Permission.forLabel(type.getName())).squash(psVal);
+ } catch (NoSuchChangeException e) {
+ // The project has disappeared.
+ //
+ return false;
+ }
+
+ if (account != null && !account.equals(approver)) {
+ return false;
+ }
+
+ if (group != null && !reviewer.getEffectiveGroups().contains(group)) {
+ return false;
+ }
+
+ if (psVal == expVal) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int getCost() {
+ return 1 + (group == null ? 0 : 1);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
index 639be517c..6832e4e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
@@ -18,15 +18,16 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
-class IsReviewedPredicate extends OperatorPredicate<ChangeData> {
+class IsReviewedPredicate extends IndexPredicate<ChangeData> {
private final Provider<ReviewDb> dbProvider;
IsReviewedPredicate(Provider<ReviewDb> dbProvider) {
- super(ChangeQueryBuilder.FIELD_IS, "reviewed");
+ super(ChangeField.REVIEWED, "1");
this.dbProvider = dbProvider;
}
@@ -51,4 +52,9 @@
public int getCost() {
return 2;
}
+
+ @Override
+ public String toString() {
+ return "is:reviewed";
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
index 3fd9feb..b0b75d0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
@@ -14,28 +14,44 @@
package com.google.gerrit.server.query.change;
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Provider;
-class IsStarredByPredicate extends OperatorPredicate<ChangeData> implements
+import java.util.List;
+import java.util.Set;
+
+class IsStarredByPredicate extends OrPredicate<ChangeData> implements
ChangeDataSource {
private static String describe(CurrentUser user) {
- if (user instanceof IdentifiedUser) {
+ if (user.isIdentifiedUser()) {
return ((IdentifiedUser) user).getAccountId().toString();
}
return user.toString();
}
+ private static List<Predicate<ChangeData>> predicates(
+ Provider<ReviewDb> db,
+ Set<Change.Id> ids) {
+ List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(ids.size());
+ for (Change.Id id : ids) {
+ r.add(new LegacyChangeIdPredicate(db, id));
+ }
+ return r;
+ }
+
private final Provider<ReviewDb> db;
private final CurrentUser user;
IsStarredByPredicate(Provider<ReviewDb> db, CurrentUser user) {
- super(ChangeQueryBuilder.FIELD_STARREDBY, describe(user));
+ super(predicates(db, user.getStarredChanges()));
this.db = db;
this.user = user;
}
@@ -65,4 +81,14 @@
public int getCost() {
return ChangeCosts.cost(ChangeCosts.IDS_MEMORY, getCardinality());
}
+
+ @Override
+ public String toString() {
+ String val = describe(user);
+ if (val.indexOf(' ') < 0) {
+ return ChangeQueryBuilder.FIELD_STARREDBY + ":" + val;
+ } else {
+ return ChangeQueryBuilder.FIELD_STARREDBY + ":\"" + val + "\"";
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
index b73465a..8992318 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
@@ -26,7 +26,7 @@
class IsVisibleToPredicate extends OperatorPredicate<ChangeData> {
private static String describe(CurrentUser user) {
- if (user instanceof IdentifiedUser) {
+ if (user.isIdentifiedUser()) {
return ((IdentifiedUser) user).getAccountId().toString();
}
if (user instanceof SingleGroupUser) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index 25cfae7..a6a344d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -14,108 +14,103 @@
package com.google.gerrit.server.query.change;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.AndPredicate;
import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryBuilder;
import com.google.gerrit.server.query.QueryParseException;
-import com.google.gwtorm.server.OrmException;
-import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
-class IsWatchedByPredicate extends OperatorPredicate<ChangeData> {
+class IsWatchedByPredicate extends AndPredicate<ChangeData> {
private static String describe(CurrentUser user) {
- if (user instanceof IdentifiedUser) {
+ if (user.isIdentifiedUser()) {
return ((IdentifiedUser) user).getAccountId().toString();
}
return user.toString();
}
- private final ChangeQueryBuilder.Arguments args;
private final CurrentUser user;
- private Map<Project.NameKey, List<Predicate<ChangeData>>> rules;
-
- IsWatchedByPredicate(ChangeQueryBuilder.Arguments args, CurrentUser user) {
- super(ChangeQueryBuilder.FIELD_WATCHEDBY, describe(user));
- this.args = args;
+ IsWatchedByPredicate(ChangeQueryBuilder.Arguments args,
+ CurrentUser user,
+ boolean checkIsVisible) {
+ super(filters(args, user, checkIsVisible));
this.user = user;
}
- @Override
- public boolean match(final ChangeData cd) throws OrmException {
- if (rules == null) {
- ChangeQueryBuilder builder = new ChangeQueryBuilder(args, user);
- rules = new HashMap<Project.NameKey, List<Predicate<ChangeData>>>();
- for (AccountProjectWatch w : user.getNotificationFilters()) {
- List<Predicate<ChangeData>> list = rules.get(w.getProjectNameKey());
- if (list == null) {
- list = new ArrayList<Predicate<ChangeData>>(4);
- rules.put(w.getProjectNameKey(), list);
- }
-
- Predicate<ChangeData> p = compile(builder, w);
- if (p != null) {
- list.add(p);
+ private static List<Predicate<ChangeData>> filters(
+ ChangeQueryBuilder.Arguments args,
+ CurrentUser user,
+ boolean checkIsVisible) {
+ List<Predicate<ChangeData>> r = Lists.newArrayList();
+ ChangeQueryBuilder builder = new ChangeQueryBuilder(args, user);
+ for (AccountProjectWatch w : user.getNotificationFilters()) {
+ Predicate<ChangeData> f = null;
+ if (w.getFilter() != null) {
+ try {
+ f = builder.parse(w.getFilter());
+ if (QueryBuilder.find(f, IsWatchedByPredicate.class) != null) {
+ // If the query is going to infinite loop, assume it
+ // will never match and return null. Yes this test
+ // prevents you from having a filter that matches what
+ // another user is filtering on. :-)
+ continue;
+ }
+ } catch (QueryParseException e) {
+ continue;
}
}
- }
- if (rules.isEmpty()) {
- return false;
- }
+ Predicate<ChangeData> p;
+ if (w.getProjectNameKey().equals(args.allProjectsName)) {
+ p = null;
+ } else {
+ p = builder.project(w.getProjectNameKey().get());
+ }
- Change change = cd.change(args.dbProvider);
- if (change == null) {
- return false;
- }
-
- Project.NameKey project = change.getDest().getParentKey();
- List<Predicate<ChangeData>> list = rules.get(project);
- if (list == null) {
- list = rules.get(args.allProjectsName);
- }
- if (list != null) {
- for (Predicate<ChangeData> p : list) {
- if (p.match(cd)) {
- return true;
- }
+ if (p != null && f != null) {
+ @SuppressWarnings("unchecked")
+ Predicate<ChangeData> andPredicate = and(p, f);
+ r.add(andPredicate);
+ } else if (p != null) {
+ r.add(p);
+ } else if (f != null) {
+ r.add(f);
+ } else {
+ r.add(builder.status_open());
}
}
- return false;
+ if (r.isEmpty()) {
+ return none();
+ } else if (checkIsVisible) {
+ return ImmutableList.of(or(r), builder.is_visible());
+ } else {
+ return ImmutableList.of(or(r));
+ }
}
- @SuppressWarnings("unchecked")
- private Predicate<ChangeData> compile(ChangeQueryBuilder builder,
- AccountProjectWatch w) {
- Predicate<ChangeData> p = builder.is_visible();
- if (w.getFilter() != null) {
- try {
- p = Predicate.and(builder.parse(w.getFilter()), p);
- if (builder.find(p, IsWatchedByPredicate.class) != null) {
- // If the query is going to infinite loop, assume it
- // will never match and return null. Yes this test
- // prevents you from having a filter that matches what
- // another user is filtering on. :-)
- //
- return null;
- }
- p = args.rewriter.get().rewrite(p);
- } catch (QueryParseException e) {
- return null;
- }
- }
- return p;
+ private static List<Predicate<ChangeData>> none() {
+ Predicate<ChangeData> any = any();
+ return ImmutableList.of(not(any));
}
@Override
public int getCost() {
return 1;
}
+
+ @Override
+ public String toString() {
+ String val = describe(user);
+ if (val.indexOf(' ') < 0) {
+ return ChangeQueryBuilder.FIELD_WATCHEDBY + ":" + val;
+ } else {
+ return ChangeQueryBuilder.FIELD_WATCHEDBY + ":\"" + val + "\"";
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
index b0d02f8..b3ea6b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -14,84 +14,109 @@
package com.google.gerrit.server.query.change;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.Permission;
+import com.google.common.collect.Lists;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.query.OrPredicate;
+import com.google.gerrit.server.query.Predicate;
import com.google.inject.Provider;
-import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-class LabelPredicate extends OperatorPredicate<ChangeData> {
+public class LabelPredicate extends OrPredicate<ChangeData> {
+ private static final int MAX_LABEL_VALUE = 4;
+
private static enum Test {
- EQ {
- @Override
- public boolean match(int psValue, int expValue) {
- return psValue == expValue;
- }
- },
- GT_EQ {
- @Override
- public boolean match(int psValue, int expValue) {
- return psValue >= expValue;
- }
- },
- LT_EQ {
- @Override
- public boolean match(int psValue, int expValue) {
- return psValue <= expValue;
- }
- };
+ EQ, GT_EQ, LT_EQ;
- abstract boolean match(int psValue, int expValue);
+ boolean isEq() {
+ return EQ.equals(this);
+ }
+
+ boolean isGtEq() {
+ return GT_EQ.equals(this);
+ }
+
+ static Test op(String op) {
+ if ("=".equals(op)) {
+ return EQ;
+
+ } else if (">=".equals(op)) {
+ return GT_EQ;
+
+ } else if ("<=".equals(op)) {
+ return LT_EQ;
+
+ } else {
+ throw new IllegalArgumentException("Unsupported operation " + op);
+ }
+ }
}
- private static LabelType type(LabelTypes types, String toFind) {
- if (types.byLabel(toFind) != null) {
- return types.byLabel(toFind);
- }
+ private final String value;
- for (LabelType lt : types.getLabelTypes()) {
- if (toFind.equalsIgnoreCase(lt.getName())) {
- return lt;
- }
- }
-
- for (LabelType lt : types.getLabelTypes()) {
- if (toFind.equalsIgnoreCase(lt.getAbbreviation())) {
- return lt;
- }
- }
-
- return LabelType.withDefaultValues(toFind);
+ LabelPredicate(ProjectCache projectCache,
+ ChangeControl.GenericFactory ccFactory,
+ IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+ String value, Set<Account.Id> accounts, AccountGroup.UUID group) {
+ super(predicates(projectCache, ccFactory, userFactory, dbProvider, value,
+ accounts, group));
+ this.value = value;
}
- private static Test op(String op) {
- if ("=".equals(op)) {
- return Test.EQ;
+ private static List<Predicate<ChangeData>> predicates(
+ ProjectCache projectCache, ChangeControl.GenericFactory ccFactory,
+ IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+ String value, Set<Account.Id> accounts, AccountGroup.UUID group) {
+ String label;
+ Test test;
+ int expVal;
+ Matcher m1 = Pattern.compile("(=|>=|<=)([+-]?\\d+)$").matcher(value);
+ Matcher m2 = Pattern.compile("([+-]\\d+)$").matcher(value);
+ if (m1.find()) {
+ label = value.substring(0, m1.start());
+ test = Test.op(m1.group(1));
+ expVal = value(m1.group(2));
- } else if (">=".equals(op)) {
- return Test.GT_EQ;
-
- } else if ("<=".equals(op)) {
- return Test.LT_EQ;
+ } else if (m2.find()) {
+ label = value.substring(0, m2.start());
+ test = Test.EQ;
+ expVal = value(m2.group(1));
} else {
- throw new IllegalArgumentException("Unsupported operation " + op);
+ label = value;
+ test = Test.EQ;
+ expVal = 1;
}
+
+ List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
+ if (test.isEq()) {
+ if (expVal != 0) {
+ r.add(equalsLabelPredicate(projectCache, ccFactory, userFactory,
+ dbProvider, label, expVal, accounts, group));
+ } else {
+ r.add(noLabelQuery(projectCache, ccFactory, userFactory,
+ dbProvider, label, accounts, group));
+ }
+ } else {
+ for (int i = test.isGtEq() ? expVal : neg(expVal); i <= MAX_LABEL_VALUE; i++) {
+ if (i != 0) {
+ r.add(equalsLabelPredicate(projectCache, ccFactory, userFactory,
+ dbProvider, label, test.isGtEq() ? i : neg(i), accounts, group));
+ } else {
+ r.add(noLabelQuery(projectCache, ccFactory, userFactory,
+ dbProvider, label, accounts, group));
+ }
+ }
+ }
+ return r;
}
private static int value(String value) {
@@ -101,113 +126,45 @@
return Integer.parseInt(value);
}
- private final ProjectCache projectCache;
- private final ChangeControl.GenericFactory ccFactory;
- private final IdentifiedUser.GenericFactory userFactory;
- private final Provider<ReviewDb> dbProvider;
- private final Test test;
- private final String type;
- private final int expVal;
+ private static int neg(int value) {
+ return -1 * value;
+ }
- LabelPredicate(ProjectCache projectCache,
+ private static Predicate<ChangeData> noLabelQuery(ProjectCache projectCache,
ChangeControl.GenericFactory ccFactory,
- IdentifiedUser.GenericFactory userFactory,
- Provider<ReviewDb> dbProvider,
- String value) {
- super(ChangeQueryBuilder.FIELD_LABEL, value);
- this.ccFactory = ccFactory;
- this.projectCache = projectCache;
- this.userFactory = userFactory;
- this.dbProvider = dbProvider;
+ IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+ String label, Set<Account.Id> accounts, AccountGroup.UUID group) {
+ List<Predicate<ChangeData>> r =
+ Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE);
+ for (int i = 1; i <= MAX_LABEL_VALUE; i++) {
+ r.add(not(equalsLabelPredicate(projectCache, ccFactory, userFactory,
+ dbProvider, label, i, accounts, group)));
+ r.add(not(equalsLabelPredicate(projectCache, ccFactory, userFactory,
+ dbProvider, label, neg(i), accounts, group)));
+ }
+ return and(r);
+ }
- Matcher m1 = Pattern.compile("(=|>=|<=)([+-]?\\d+)$").matcher(value);
- Matcher m2 = Pattern.compile("([+-]\\d+)$").matcher(value);
- if (m1.find()) {
- type = value.substring(0, m1.start());
- test = op(m1.group(1));
- expVal = value(m1.group(2));
-
- } else if (m2.find()) {
- type = value.substring(0, m2.start());
- test = Test.EQ;
- expVal = value(m2.group(1));
-
+ private static Predicate<ChangeData> equalsLabelPredicate(
+ ProjectCache projectCache, ChangeControl.GenericFactory ccFactory,
+ IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
+ String label, int expVal, Set<Account.Id> accounts,
+ AccountGroup.UUID group) {
+ if (accounts == null || accounts.isEmpty()) {
+ return new EqualsLabelPredicate(projectCache, ccFactory, userFactory,
+ dbProvider, label, expVal, null, group);
} else {
- type = value;
- test = Test.EQ;
- expVal = 1;
+ List<Predicate<ChangeData>> r = Lists.newArrayList();
+ for (Account.Id a : accounts) {
+ r.add(new EqualsLabelPredicate(projectCache, ccFactory, userFactory,
+ dbProvider, label, expVal, a, group));
+ }
+ return or(r);
}
}
@Override
- public boolean match(final ChangeData object) throws OrmException {
- final Change c = object.change(dbProvider);
- if (c == null) {
- // The change has disappeared.
- //
- return false;
- }
- final ProjectState project = projectCache.get(c.getDest().getParentKey());
- if (project == null) {
- // The project has disappeared.
- //
- return false;
- }
- final LabelType labelType = type(project.getLabelTypes(), type);
- final Set<Account.Id> allApprovers = new HashSet<Account.Id>();
- final Set<Account.Id> approversThatVotedInCategory = new HashSet<Account.Id>();
- for (PatchSetApproval p : object.currentApprovals(dbProvider)) {
- allApprovers.add(p.getAccountId());
- if (labelType.matches(p)) {
- approversThatVotedInCategory.add(p.getAccountId());
- if (match(c, p.getValue(), p.getAccountId(), labelType)) {
- return true;
- }
- }
- }
-
- final Set<Account.Id> approversThatDidNotVoteInCategory = new HashSet<Account.Id>(allApprovers);
- approversThatDidNotVoteInCategory.removeAll(approversThatVotedInCategory);
- for (Account.Id a : approversThatDidNotVoteInCategory) {
- if (match(c, 0, a, labelType)) {
- return true;
- }
- }
-
- return false;
- }
-
- private boolean match(final Change change, final int value,
- final Account.Id approver, final LabelType type)
- throws OrmException {
- int psVal = value;
- if (test.match(psVal, expVal)) {
- // Double check the value is still permitted for the user.
- //
- try {
- ChangeControl cc = ccFactory.controlFor(change, //
- userFactory.create(dbProvider, approver));
- if (!cc.isVisible(dbProvider.get())) {
- // The user can't see the change anymore.
- //
- return false;
- }
- psVal = cc.getRange(Permission.forLabel(type.getName())).squash(psVal);
- } catch (NoSuchChangeException e) {
- // The project has disappeared.
- //
- return false;
- }
-
- if (test.match(psVal, expVal)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public int getCost() {
- return 2;
+ public String toString() {
+ return ChangeQueryBuilder.FIELD_LABEL + ":" + value;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
index 4c47e73..48ae48f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -16,7 +16,8 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.ListResultSet;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
@@ -24,13 +25,13 @@
import java.util.Collections;
-class LegacyChangeIdPredicate extends OperatorPredicate<ChangeData> implements
+class LegacyChangeIdPredicate extends IndexPredicate<ChangeData> implements
ChangeDataSource {
private final Provider<ReviewDb> db;
private final Change.Id id;
LegacyChangeIdPredicate(Provider<ReviewDb> db, Change.Id id) {
- super(ChangeQueryBuilder.FIELD_CHANGE, id.toString());
+ super(ChangeField.LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
this.db = db;
this.id = id;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyMessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyMessagePredicate.java
new file mode 100644
index 0000000..6b6d1e5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyMessagePredicate.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.revwalk.filter.MessageRevFilter;
+import org.eclipse.jgit.revwalk.filter.RevFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * Predicate to match changes that contains specified text in commit messages
+ * body.
+ */
+public class LegacyMessagePredicate extends RevWalkPredicate {
+
+ private static final Logger log = LoggerFactory
+ .getLogger(LegacyMessagePredicate.class);
+
+ private final RevFilter rFilter;
+
+ public LegacyMessagePredicate(Provider<ReviewDb> db,
+ GitRepositoryManager repoManager, String text) {
+ super(db, repoManager, ChangeQueryBuilder.FIELD_MESSAGE, text);
+ this.rFilter = MessageRevFilter.create(text);
+ }
+
+ @Override
+ public boolean match(Repository repo, RevWalk rw, Arguments args) {
+ try {
+ return rFilter.include(rw, rw.parseCommit(args.objectId));
+ } catch (MissingObjectException e) {
+ log.error(args.projectName.get() + "\" commit does not exist.", e);
+ } catch (IncorrectObjectTypeException e) {
+ log.error(args.projectName.get() + "\" revision is not a commit.", e);
+ } catch (IOException e) {
+ log.error(
+ "Could not search for commit message in \"" + args.projectName.get()
+ + "\" repository.", e);
+ }
+ return false;
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
index 0ea280d..64e6b10 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -15,49 +15,43 @@
package com.google.gerrit.server.query.change;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.revwalk.filter.MessageRevFilter;
-import org.eclipse.jgit.revwalk.filter.RevFilter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-
/**
* Predicate to match changes that contains specified text in commit messages
* body.
*/
-public class MessagePredicate extends RevWalkPredicate {
+class MessagePredicate extends IndexPredicate<ChangeData> {
+ private final Provider<ReviewDb> db;
+ private final ChangeIndex index;
- private static final Logger log =
- LoggerFactory.getLogger(MessagePredicate.class);
-
- private final RevFilter rFilter;
-
- public MessagePredicate(Provider<ReviewDb> db,
- GitRepositoryManager repoManager, String text) {
- super(db, repoManager, ChangeQueryBuilder.FIELD_MESSAGE, text);
- this.rFilter = MessageRevFilter.create(text);
+ MessagePredicate(Provider<ReviewDb> db, ChangeIndex index, String value) {
+ super(ChangeField.COMMIT_MESSAGE, value);
+ this.db = db;
+ this.index = index;
}
+ @SuppressWarnings("unchecked")
@Override
- public boolean match(Repository repo, RevWalk rw, Arguments args) {
+ public boolean match(ChangeData object) throws OrmException {
try {
- return rFilter.include(rw, rw.parseCommit(args.objectId));
- } catch (MissingObjectException e) {
- log.error(args.projectName.get() + "\" commit does not exist.", e);
- } catch (IncorrectObjectTypeException e) {
- log.error(args.projectName.get() + "\" revision is not a commit.", e);
- } catch (IOException e) {
- log.error("Could not search for commit message in \"" +
- args.projectName.get() + "\" repository.", e);
+ for (ChangeData cData : index.getSource(
+ Predicate.and(new LegacyChangeIdPredicate(db, object.getId()), this), 1)
+ .read()) {
+ if (cData.getId().equals(object.getId())) {
+ return true;
+ }
+ }
+ } catch (QueryParseException e) {
+ throw new OrmException(e);
}
+
return false;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
index ec5195b..4f36777 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
@@ -25,10 +25,10 @@
import java.util.Collection;
import java.util.HashSet;
-class OrSource extends OrPredicate<ChangeData> implements ChangeDataSource {
+public class OrSource extends OrPredicate<ChangeData> implements ChangeDataSource {
private int cardinality = -1;
- OrSource(final Collection<? extends Predicate<ChangeData>> that) {
+ public OrSource(Collection<? extends Predicate<ChangeData>> that) {
super(that);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
index 7a85ef6..10f2d35 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
@@ -17,16 +17,17 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
-class OwnerPredicate extends OperatorPredicate<ChangeData> {
+class OwnerPredicate extends IndexPredicate<ChangeData> {
private final Provider<ReviewDb> dbProvider;
private final Account.Id id;
OwnerPredicate(Provider<ReviewDb> dbProvider, Account.Id id) {
- super(ChangeQueryBuilder.FIELD_OWNER, id.toString());
+ super(ChangeField.OWNER, id.toString());
this.dbProvider = dbProvider;
this.id = id;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java
new file mode 100644
index 0000000..2d2f254
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PredicateArgs.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.query.QueryParseException;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class is used to extract comma separated values in a predicate
+ *
+ * If tags for the values are present (e.g. "branch=jb_2.3,vote=approved") then
+ * the args are placed in a map that maps tag to value (e.g., "branch" to "jb_2.3").
+ * If no tag is present (e.g. "jb_2.3,approved") then the args are placed into a
+ * positional list. Args may be mixed so some may appear in the map and others
+ * in the positional list (e.g. "vote=approved,jb_2.3).
+ */
+public class PredicateArgs {
+ public List<String> positional;
+ public Map<String, String> keyValue;
+
+ /**
+ * Parses query arguments into keyValue and/or positional values
+ * labels for these arguments should be kept in ChangeQueryBuilder
+ * as ARG_ID_{argument name}.
+ *
+ * @param args - arguments to be parsed
+ *
+ * @return - the static values keyValue and positional will contain
+ * the parsed values.
+ * @throws QueryParseException
+ */
+ PredicateArgs(String args) throws QueryParseException {
+ positional = Lists.newArrayList();
+ keyValue = Maps.newHashMap();
+
+ String[] splitArgs = args.split(",");
+
+ for (String arg : splitArgs) {
+ String[] splitKeyValue = arg.split("=");
+
+ if (splitKeyValue.length == 1) {
+ positional.add(splitKeyValue[0]);
+ } else if (splitKeyValue.length == 2) {
+ if (!keyValue.containsKey(splitKeyValue[0])) {
+ keyValue.put(splitKeyValue[0], splitKeyValue[1]);
+ } else {
+ throw new QueryParseException("Duplicate key " + splitKeyValue[0]);
+ }
+ } else {
+ throw new QueryParseException("invalid arg " + arg);
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
index cce2f2a..fe8d937 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
@@ -17,15 +17,16 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
-class ProjectPredicate extends OperatorPredicate<ChangeData> {
+class ProjectPredicate extends IndexPredicate<ChangeData> {
private final Provider<ReviewDb> dbProvider;
ProjectPredicate(Provider<ReviewDb> dbProvider, String id) {
- super(ChangeQueryBuilder.FIELD_PROJECT, id);
+ super(ChangeField.PROJECT, id);
this.dbProvider = dbProvider;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
index a005940..24186b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -20,13 +20,15 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.ssh.SshInfo;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import org.kohsuke.args4j.Option;
@@ -40,6 +42,7 @@
public class QueryChanges implements RestReadView<TopLevelResource> {
private final ChangeJson json;
private final QueryProcessor imp;
+ private final Provider<CurrentUser> user;
private boolean reverse;
private EnumSet<ListChangesOption> options;
@@ -81,13 +84,13 @@
@Inject
QueryChanges(ChangeJson json,
QueryProcessor qp,
- SshInfo sshInfo,
- ChangeControl.Factory cf) {
+ ChangeControl.Factory cf,
+ Provider<CurrentUser> user) {
this.json = json;
this.imp = qp;
+ this.user = user;
options = EnumSet.noneOf(ListChangesOption.class);
- json.setSshInfo(sshInfo);
json.setChangeControlFactory(cf);
}
@@ -130,21 +133,36 @@
throw new QueryParseException("limit of 10 queries");
}
+ IdentifiedUser self = null;
+ try {
+ if (user.get().isIdentifiedUser()) {
+ self = (IdentifiedUser) user.get();
+ self.asyncStarredChanges();
+ }
+ return query0();
+ } finally {
+ if (self != null) {
+ self.abortStarredChanges();
+ }
+ }
+ }
+
+ private List<List<ChangeInfo>> query0() throws OrmException,
+ QueryParseException {
int cnt = queries.size();
BitSet more = new BitSet(cnt);
- List<List<ChangeData>> data = Lists.newArrayListWithCapacity(cnt);
+ List<List<ChangeData>> data = imp.queryChanges(queries);
for (int n = 0; n < cnt; n++) {
- String query = queries.get(n);
- List<ChangeData> changes = imp.queryChanges(query);
+ List<ChangeData> changes = data.get(n);
if (imp.getLimit() > 0 && changes.size() > imp.getLimit()) {
if (reverse) {
changes = changes.subList(1, changes.size());
} else {
changes = changes.subList(0, imp.getLimit());
}
+ data.set(n, changes);
more.set(n, true);
}
- data.add(changes);
}
List<List<ChangeInfo>> res = json.addOptions(options).formatList2(data);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index 06d84c1..bdce7f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -14,10 +14,11 @@
package com.google.gerrit.server.query.change;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
@@ -32,6 +33,7 @@
import com.google.gerrit.server.query.QueryParseException;
import com.google.gson.Gson;
import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -52,7 +54,6 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
-import java.util.HashSet;
import java.util.List;
public class QueryProcessor {
@@ -211,47 +212,70 @@
* there are more than {@code limit} matches and suggest to its own caller
* that the query could be retried with {@link #setSortkeyBefore(String)}.
*/
- public List<ChangeData> queryChanges(final String queryString)
+ public List<ChangeData> queryChanges(String queryString)
+ throws OrmException, QueryParseException {
+ return queryChanges(ImmutableList.of(queryString)).get(0);
+ }
+
+ /**
+ * Query for changes that match the query string.
+ * <p>
+ * If a limit was specified using {@link #setLimit(int)} this method may
+ * return up to {@code limit + 1} results, allowing the caller to determine if
+ * there are more than {@code limit} matches and suggest to its own caller
+ * that the query could be retried with {@link #setSortkeyBefore(String)}.
+ */
+ public List<List<ChangeData>> queryChanges(List<String> queries)
throws OrmException, QueryParseException {
final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
- Predicate<ChangeData> s = compileQuery(queryString, visibleToMe);
- List<ChangeData> results = new ArrayList<ChangeData>();
- HashSet<Change.Id> want = new HashSet<Change.Id>();
- for (ChangeData d : ((ChangeDataSource) s).read()) {
- if (d.hasChange()) {
- // Checking visibleToMe here should be unnecessary, the
- // query should have already performed it. But we don't
- // want to trust the query rewriter that much yet.
- //
- if (visibleToMe.match(d)) {
- results.add(d);
- }
- } else {
- want.add(d.getId());
+ int cnt = queries.size();
+
+ // Parse and rewrite all queries.
+ List<Integer> limits = Lists.newArrayListWithCapacity(cnt);
+ List<ChangeDataSource> sources = Lists.newArrayListWithCapacity(cnt);
+ for (int i = 0; i < cnt; i++) {
+ Predicate<ChangeData> q = parseQuery(queries.get(i), visibleToMe);
+ Predicate<ChangeData> s = queryRewriter.rewrite(q);
+ if (!(s instanceof ChangeDataSource)) {
+ @SuppressWarnings("unchecked")
+ Predicate<ChangeData> o = Predicate.and(queryBuilder.status_open(), q);
+ q = o;
+ s = queryRewriter.rewrite(q);
}
+ if (!(s instanceof ChangeDataSource)) {
+ throw new QueryParseException("invalid query: " + s);
+ }
+
+ // Don't trust QueryRewriter to have left the visible predicate.
+ AndSource a = new AndSource(db, ImmutableList.of(s, visibleToMe));
+ limits.add(limit(q));
+ sources.add(a);
}
- if (!want.isEmpty()) {
- for (Change c : db.get().changes().get(want)) {
- ChangeData d = new ChangeData(c);
- if (visibleToMe.match(d)) {
- results.add(d);
- }
- }
+ // Run each query asynchronously, if supported.
+ List<ResultSet<ChangeData>> matches = Lists.newArrayListWithCapacity(cnt);
+ for (ChangeDataSource s : sources) {
+ matches.add(s.read());
}
+ sources = null;
- Collections.sort(results, sortkeyAfter != null ? cmpAfter : cmpBefore);
- int limit = limit(s);
- if (results.size() > maxLimit) {
- moreResults = true;
+ List<List<ChangeData>> out = Lists.newArrayListWithCapacity(cnt);
+ for (int i = 0; i < cnt; i++) {
+ List<ChangeData> results = matches.get(i).toList();
+ Collections.sort(results, sortkeyAfter != null ? cmpAfter : cmpBefore);
+ if (results.size() > maxLimit) {
+ moreResults = true;
+ }
+ int limit = limits.get(i);
+ if (limit < results.size()) {
+ results = results.subList(0, limit);
+ }
+ if (sortkeyAfter != null) {
+ Collections.reverse(results);
+ }
+ out.add(results);
}
- if (limit < results.size()) {
- results = results.subList(0, limit);
- }
- if (sortkeyAfter != null) {
- Collections.reverse(results);
- }
- return results;
+ return out;
}
public void query(String queryString) throws IOException {
@@ -371,16 +395,17 @@
}
private int limit(Predicate<ChangeData> s) {
- int n = queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : maxLimit;
+ int n = ChangeQueryBuilder.hasLimit(s)
+ ? ChangeQueryBuilder.getLimit(s)
+ : maxLimit;
return limit > 0 ? Math.min(n, limit) + 1 : n + 1;
}
@SuppressWarnings("unchecked")
- private Predicate<ChangeData> compileQuery(String queryString,
+ private Predicate<ChangeData> parseQuery(String queryString,
final Predicate<ChangeData> visibleToMe) throws QueryParseException {
-
Predicate<ChangeData> q = queryBuilder.parse(queryString);
- if (!queryBuilder.hasSortKey(q)) {
+ if (!ChangeQueryBuilder.hasSortKey(q)) {
if (sortkeyBefore != null) {
q = Predicate.and(q, queryBuilder.sortkey_before(sortkeyBefore));
} else if (sortkeyAfter != null) {
@@ -389,20 +414,9 @@
q = Predicate.and(q, queryBuilder.sortkey_before("z"));
}
}
- q = Predicate.and(q,
+ return Predicate.and(q,
queryBuilder.limit(limit > 0 ? Math.min(limit, maxLimit) + 1 : maxLimit),
visibleToMe);
-
- Predicate<ChangeData> s = queryRewriter.rewrite(q);
- if (!(s instanceof ChangeDataSource)) {
- s = queryRewriter.rewrite(Predicate.and(queryBuilder.status_open(), q));
- }
-
- if (!(s instanceof ChangeDataSource)) {
- throw new QueryParseException("invalid query: " + s);
- }
-
- return s;
}
private void show(Object data) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
index 4811f9a..df0150f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
@@ -16,15 +16,16 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
-class RefPredicate extends OperatorPredicate<ChangeData> {
+class RefPredicate extends IndexPredicate<ChangeData> {
private final Provider<ReviewDb> dbProvider;
RefPredicate(Provider<ReviewDb> dbProvider, String ref) {
- super(ChangeQueryBuilder.FIELD_REF, ref);
+ super(ChangeField.REF, ref);
this.dbProvider = dbProvider;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java
deleted file mode 100644
index 6704a10..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
-
-import dk.brics.automaton.RegExp;
-import dk.brics.automaton.RunAutomaton;
-
-class RegexBranchPredicate extends OperatorPredicate<ChangeData> {
- private final Provider<ReviewDb> dbProvider;
- private final RunAutomaton pattern;
-
- RegexBranchPredicate(Provider<ReviewDb> dbProvider, String re) {
- super(ChangeQueryBuilder.FIELD_BRANCH, re);
-
- if (re.startsWith("^")) {
- re = re.substring(1);
- }
-
- if (re.endsWith("$") && !re.endsWith("\\$")) {
- re = re.substring(0, re.length() - 1);
- }
-
- this.dbProvider = dbProvider;
- this.pattern = new RunAutomaton(new RegExp(re).toAutomaton());
- }
-
- @Override
- public boolean match(final ChangeData object) throws OrmException {
- Change change = object.change(dbProvider);
- if (change == null) {
- return false;
- }
- return change.getDest().get().startsWith(Branch.R_HEADS)
- && pattern.run(change.getDest().getShortName());
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java
index 11856e4..1ce9139 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java
@@ -15,8 +15,9 @@
package com.google.gerrit.server.query.change;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.RegexPredicate;
import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.query.OperatorPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
@@ -24,9 +25,10 @@
import dk.brics.automaton.RegExp;
import dk.brics.automaton.RunAutomaton;
-import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
-class RegexFilePredicate extends OperatorPredicate<ChangeData> {
+class RegexFilePredicate extends RegexPredicate<ChangeData> {
private final Provider<ReviewDb> db;
private final PatchListCache cache;
private final RunAutomaton pattern;
@@ -37,7 +39,7 @@
private final boolean prefixOnly;
RegexFilePredicate(Provider<ReviewDb> db, PatchListCache plc, String re) {
- super(ChangeQueryBuilder.FIELD_FILE, re);
+ super(ChangeField.FILE, re);
this.db = db;
this.cache = plc;
@@ -67,7 +69,7 @@
@Override
public boolean match(ChangeData object) throws OrmException {
- String[] files = object.currentFilePaths(db, cache);
+ List<String> files = object.currentFilePaths(db, cache);
if (files != null) {
int begin, end;
@@ -76,7 +78,7 @@
end = find(files, prefixEnd);
} else {
begin = 0;
- end = files.length;
+ end = files.size();
}
if (prefixOnly) {
@@ -84,7 +86,7 @@
}
while (begin < end) {
- if (pattern.run(files[begin++])) {
+ if (pattern.run(files.get(begin++))) {
return true;
}
}
@@ -100,8 +102,8 @@
}
}
- private static int find(String[] files, String p) {
- int r = Arrays.binarySearch(files, p);
+ private static int find(List<String> files, String p) {
+ int r = Collections.binarySearch(files, p);
return r < 0 ? -(r + 1) : r;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
index b8911a4..b6c724d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
@@ -17,19 +17,20 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.RegexPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
import dk.brics.automaton.RegExp;
import dk.brics.automaton.RunAutomaton;
-class RegexProjectPredicate extends OperatorPredicate<ChangeData> {
+class RegexProjectPredicate extends RegexPredicate<ChangeData> {
private final Provider<ReviewDb> dbProvider;
private final RunAutomaton pattern;
RegexProjectPredicate(Provider<ReviewDb> dbProvider, String re) {
- super(ChangeQueryBuilder.FIELD_PROJECT, re);
+ super(ChangeField.PROJECT, re);
if (re.startsWith("^")) {
re = re.substring(1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
index 1480de6..22fb49b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
@@ -16,19 +16,20 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.RegexPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
import dk.brics.automaton.RegExp;
import dk.brics.automaton.RunAutomaton;
-class RegexRefPredicate extends OperatorPredicate<ChangeData> {
+class RegexRefPredicate extends RegexPredicate<ChangeData> {
private final Provider<ReviewDb> dbProvider;
private final RunAutomaton pattern;
RegexRefPredicate(Provider<ReviewDb> dbProvider, String re) {
- super(ChangeQueryBuilder.FIELD_REF, re);
+ super(ChangeField.REF, re);
if (re.startsWith("^")) {
re = re.substring(1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
index 03814f8..51b9c48 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
@@ -16,19 +16,20 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.RegexPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
import dk.brics.automaton.RegExp;
import dk.brics.automaton.RunAutomaton;
-class RegexTopicPredicate extends OperatorPredicate<ChangeData> {
+class RegexTopicPredicate extends RegexPredicate<ChangeData> {
private final Provider<ReviewDb> dbProvider;
private final RunAutomaton pattern;
RegexTopicPredicate(Provider<ReviewDb> dbProvider, String re) {
- super(ChangeQueryBuilder.FIELD_TOPIC, re);
+ super(ChangeField.TOPIC, re);
if (re.startsWith("^")) {
re = re.substring(1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index 8e910df..9e9d8bf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -17,16 +17,17 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
-class ReviewerPredicate extends OperatorPredicate<ChangeData> {
+class ReviewerPredicate extends IndexPredicate<ChangeData> {
private final Provider<ReviewDb> dbProvider;
private final Account.Id id;
ReviewerPredicate(Provider<ReviewDb> dbProvider, Account.Id id) {
- super(ChangeQueryBuilder.FIELD_REVIEWER, id.toString());
+ super(ChangeField.REVIEWER, id.toString());
this.dbProvider = dbProvider;
this.id = id;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
index e7668c6..35a42f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
@@ -16,15 +16,18 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
-abstract class SortKeyPredicate extends OperatorPredicate<ChangeData> {
+public abstract class SortKeyPredicate extends IndexPredicate<ChangeData> {
protected final Provider<ReviewDb> dbProvider;
+ @SuppressWarnings("deprecation")
SortKeyPredicate(Provider<ReviewDb> dbProvider, String name, String value) {
- super(name, value);
+ super(ChangeField.SORTKEY, name, value);
this.dbProvider = dbProvider;
}
@@ -33,24 +36,47 @@
return 1;
}
- static class Before extends SortKeyPredicate {
+ public abstract long getMinValue();
+ public abstract long getMaxValue();
+
+ public static class Before extends SortKeyPredicate {
Before(Provider<ReviewDb> dbProvider, String value) {
super(dbProvider, "sortkey_before", value);
}
@Override
+ public long getMinValue() {
+ return 0;
+ }
+
+ @Override
+ public long getMaxValue() {
+ return ChangeUtil.parseSortKey(getValue());
+ }
+
+ @Override
public boolean match(ChangeData cd) throws OrmException {
Change change = cd.change(dbProvider);
return change != null && change.getSortKey().compareTo(getValue()) < 0;
}
}
- static class After extends SortKeyPredicate {
+ public static class After extends SortKeyPredicate {
After(Provider<ReviewDb> dbProvider, String value) {
super(dbProvider, "sortkey_after", value);
}
@Override
+ public long getMinValue() {
+ return ChangeUtil.parseSortKey(getValue());
+ }
+
+ @Override
+ public long getMaxValue() {
+ return Long.MAX_VALUE;
+ }
+
+ @Override
public boolean match(ChangeData cd) throws OrmException {
Change change = cd.change(dbProvider);
return change != null && change.getSortKey().compareTo(getValue()) > 0;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SqlRewriterImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SqlRewriterImpl.java
new file mode 100644
index 0000000..71daa9c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SqlRewriterImpl.java
@@ -0,0 +1,633 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.query.IntPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryRewriter;
+import com.google.gerrit.server.query.RewritePredicate;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+
+import java.util.Collection;
+
+public class SqlRewriterImpl extends BasicChangeRewrites
+ implements ChangeQueryRewriter {
+ private static final QueryRewriter.Definition<ChangeData, SqlRewriterImpl> mydef =
+ new QueryRewriter.Definition<ChangeData, SqlRewriterImpl>(
+ SqlRewriterImpl.class, BUILDER);
+
+ @Inject
+ @VisibleForTesting
+ public SqlRewriterImpl(Provider<ReviewDb> dbProvider) {
+ super(mydef, dbProvider);
+ }
+
+ @Override
+ public Predicate<ChangeData> and(Collection<? extends Predicate<ChangeData>> l) {
+ return hasSource(l) ? new AndSource(dbProvider, l) : super.and(l);
+ }
+
+ @Override
+ public Predicate<ChangeData> or(Collection<? extends Predicate<ChangeData>> l) {
+ return hasSource(l) ? new OrSource(l) : super.or(l);
+ }
+
+ @Rewrite("status:open P=(project:*) B=(ref:*)")
+ public Predicate<ChangeData> r05_byBranchOpen(
+ @Named("P") final ProjectPredicate p,
+ @Named("B") final RefPredicate b) {
+ return new ChangeSource(500) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a)
+ throws OrmException {
+ return a.byBranchOpenAll(
+ new Branch.NameKey(p.getValueKey(), b.getValue()));
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isOpen()
+ && p.match(cd)
+ && b.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:merged P=(project:*) B=(ref:*) S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r05_byBranchMergedPrev(
+ @Named("P") final ProjectPredicate p,
+ @Named("B") final RefPredicate b,
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byBranchClosedPrev(Change.Status.MERGED.getCode(), //
+ new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+ && p.match(cd) //
+ && b.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:merged P=(project:*) B=(ref:*) S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r05_byBranchMergedNext(
+ @Named("P") final ProjectPredicate p,
+ @Named("B") final RefPredicate b,
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byBranchClosedNext(Change.Status.MERGED.getCode(), //
+ new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+ && p.match(cd) //
+ && b.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:open P=(project:*) S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r10_byProjectOpenPrev(
+ @Named("P") final ProjectPredicate p,
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(500, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byProjectOpenPrev(p.getValueKey(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isOpen() //
+ && p.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:open P=(project:*) S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r10_byProjectOpenNext(
+ @Named("P") final ProjectPredicate p,
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(500, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byProjectOpenNext(p.getValueKey(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isOpen() //
+ && p.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:merged P=(project:*) S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r10_byProjectMergedPrev(
+ @Named("P") final ProjectPredicate p,
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byProjectClosedPrev(Change.Status.MERGED.getCode(), //
+ p.getValueKey(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+ && p.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:merged P=(project:*) S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r10_byProjectMergedNext(
+ @Named("P") final ProjectPredicate p,
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byProjectClosedNext(Change.Status.MERGED.getCode(), //
+ p.getValueKey(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+ && p.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:abandoned P=(project:*) S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r10_byProjectAbandonedPrev(
+ @Named("P") final ProjectPredicate p,
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byProjectClosedPrev(Change.Status.ABANDONED.getCode(), //
+ p.getValueKey(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+ && p.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:abandoned P=(project:*) S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r10_byProjectAbandonedNext(
+ @Named("P") final ProjectPredicate p,
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.byProjectClosedNext(Change.Status.ABANDONED.getCode(), //
+ p.getValueKey(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+ && p.match(cd) //
+ && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:open S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byOpenPrev(
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(2000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.allOpenPrev(key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
+ }
+ };
+ }
+
+ @Rewrite("status:open S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byOpenNext(
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(2000, s.getValue(), l.intValue()) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.allOpenNext(key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isOpen() && s.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:merged S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byMergedPrev(
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+ {
+ init("r20_byMergedPrev", s, l);
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.allClosedPrev(Change.Status.MERGED.getCode(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+ && s.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:merged S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byMergedNext(
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+ {
+ init("r20_byMergedNext", s, l);
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.allClosedNext(Change.Status.MERGED.getCode(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+ && s.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:abandoned S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byAbandonedPrev(
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+ {
+ init("r20_byAbandonedPrev", s, l);
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.allClosedPrev(Change.Status.ABANDONED.getCode(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+ && s.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:abandoned S=(sortkey_before:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byAbandonedNext(
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return new PaginatedSource(50000, s.getValue(), l.intValue()) {
+ {
+ init("r20_byAbandonedNext", s, l);
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException {
+ return a.allClosedNext(Change.Status.ABANDONED.getCode(), key, limit);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.ABANDONED
+ && s.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byClosedPrev(
+ @Named("S") final SortKeyPredicate.After s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return or(r20_byMergedPrev(s, l), r20_byAbandonedPrev(s, l));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:closed S=(sortkey_after:*) L=(limit:*)")
+ public Predicate<ChangeData> r20_byClosedNext(
+ @Named("S") final SortKeyPredicate.Before s,
+ @Named("L") final IntPredicate<ChangeData> l) {
+ return or(r20_byMergedNext(s, l), r20_byAbandonedNext(s, l));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:open O=(owner:*)")
+ public Predicate<ChangeData> r25_byOwnerOpen(
+ @Named("O") final OwnerPredicate o) {
+ return new ChangeSource(50) {
+ {
+ init("r25_byOwnerOpen", o);
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+ return a.byOwnerOpen(o.getAccountId());
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isOpen() && o.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:closed O=(owner:*)")
+ public Predicate<ChangeData> r25_byOwnerClosed(
+ @Named("O") final OwnerPredicate o) {
+ return new ChangeSource(5000) {
+ {
+ init("r25_byOwnerClosed", o);
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+ return a.byOwnerClosedAll(o.getAccountId());
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus().isClosed() && o.match(cd);
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("O=(owner:*)")
+ public Predicate<ChangeData> r26_byOwner(@Named("O") OwnerPredicate o) {
+ return or(r25_byOwnerOpen(o), r25_byOwnerClosed(o));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:open R=(reviewer:*)")
+ public Predicate<ChangeData> r30_byReviewerOpen(
+ @Named("R") final ReviewerPredicate r) {
+ return new Source() {
+ {
+ init("r30_byReviewerOpen", r);
+ }
+
+ @Override
+ public ResultSet<ChangeData> read() throws OrmException {
+ return ChangeDataResultSet.patchSetApproval(dbProvider.get()
+ .patchSetApprovals().openByUser(r.getAccountId()));
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ Change change = cd.change(dbProvider);
+ return change != null && change.getStatus().isOpen() && r.match(cd);
+ }
+
+ @Override
+ public int getCardinality() {
+ return 50;
+ }
+
+ @Override
+ public int getCost() {
+ return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("status:closed R=(reviewer:*)")
+ public Predicate<ChangeData> r30_byReviewerClosed(
+ @Named("R") final ReviewerPredicate r) {
+ return new Source() {
+ {
+ init("r30_byReviewerClosed", r);
+ }
+
+ @Override
+ public ResultSet<ChangeData> read() throws OrmException {
+ return ChangeDataResultSet.patchSetApproval(dbProvider.get()
+ .patchSetApprovals().closedByUserAll(r.getAccountId()));
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ Change change = cd.change(dbProvider);
+ return change != null && change.getStatus().isClosed() && r.match(cd);
+ }
+
+ @Override
+ public int getCardinality() {
+ return 5000;
+ }
+
+ @Override
+ public int getCost() {
+ return ChangeCosts.cost(ChangeCosts.APPROVALS_SCAN, getCardinality());
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ @Rewrite("R=(reviewer:*)")
+ public Predicate<ChangeData> r31_byReviewer(
+ @Named("R") final ReviewerPredicate r) {
+ return or(r30_byReviewerOpen(r), r30_byReviewerClosed(r));
+ }
+
+ @Rewrite("status:submitted")
+ public Predicate<ChangeData> r99_allSubmitted() {
+ return new ChangeSource(50) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+ return a.allSubmitted();
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.change(dbProvider).getStatus() == Change.Status.SUBMITTED;
+ }
+ };
+ }
+
+ @Rewrite("P=(project:*)")
+ public Predicate<ChangeData> r99_byProject(
+ @Named("P") final ProjectPredicate p) {
+ return new ChangeSource(1000000) {
+ @Override
+ ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+ return a.byProject(p.getValueKey());
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return p.match(cd);
+ }
+ };
+ }
+
+ private static boolean hasSource(Collection<? extends Predicate<ChangeData>> l) {
+ for (Predicate<ChangeData> p : l) {
+ if (p instanceof ChangeDataSource) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private abstract static class Source extends RewritePredicate<ChangeData>
+ implements ChangeDataSource {
+ @Override
+ public boolean hasChange() {
+ return false;
+ }
+ }
+
+ private abstract class ChangeSource extends Source {
+ private final int cardinality;
+
+ ChangeSource(int card) {
+ this.cardinality = card;
+ }
+
+ abstract ResultSet<Change> scan(ChangeAccess a) throws OrmException;
+
+ @Override
+ public ResultSet<ChangeData> read() throws OrmException {
+ return ChangeDataResultSet.change(scan(dbProvider.get().changes()));
+ }
+
+ @Override
+ public boolean hasChange() {
+ return true;
+ }
+
+ @Override
+ public int getCardinality() {
+ return cardinality;
+ }
+
+ @Override
+ public int getCost() {
+ return ChangeCosts.cost(ChangeCosts.CHANGES_SCAN, getCardinality());
+ }
+ }
+
+ private abstract class PaginatedSource extends ChangeSource implements
+ Paginated {
+ private final String startKey;
+ private final int limit;
+
+ PaginatedSource(int card, String start, int lim) {
+ super(card);
+ this.startKey = start;
+ this.limit = lim;
+ }
+
+ @Override
+ public int limit() {
+ return limit;
+ }
+
+ @Override
+ ResultSet<Change> scan(ChangeAccess a) throws OrmException {
+ return scan(a, startKey, limit);
+ }
+
+ @Override
+ public ResultSet<ChangeData> restart(ChangeData last) throws OrmException {
+ return ChangeDataResultSet.change(scan(dbProvider.get().changes(), //
+ last.change(dbProvider).getSortKey(), //
+ limit));
+ }
+
+ abstract ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+ throws OrmException;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
index 8d58376..9393fe1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
@@ -16,15 +16,16 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
-class TopicPredicate extends OperatorPredicate<ChangeData> {
+class TopicPredicate extends IndexPredicate<ChangeData> {
private final Provider<ReviewDb> dbProvider;
TopicPredicate(Provider<ReviewDb> dbProvider, String topic) {
- super(ChangeQueryBuilder.FIELD_TOPIC, topic);
+ super(ChangeField.TOPIC, topic);
this.dbProvider = dbProvider;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
index e022f86..38b3d0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
@@ -17,7 +17,8 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.TrackingId;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.ListResultSet;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
@@ -26,12 +27,12 @@
import java.util.ArrayList;
import java.util.HashSet;
-class TrackingIdPredicate extends OperatorPredicate<ChangeData> implements
+class TrackingIdPredicate extends IndexPredicate<ChangeData> implements
ChangeDataSource {
private final Provider<ReviewDb> db;
TrackingIdPredicate(Provider<ReviewDb> db, String trackingId) {
- super(ChangeQueryBuilder.FIELD_TR, trackingId);
+ super(ChangeField.TR, trackingId);
this.db = db;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
index 4066ad3..9aeda09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
@@ -24,6 +24,7 @@
bind(DataSourceType.class).annotatedWith(Names.named("h2")).to(H2.class);
bind(DataSourceType.class).annotatedWith(Names.named("jdbc")).to(JDBC.class);
bind(DataSourceType.class).annotatedWith(Names.named("mysql")).to(MySql.class);
+ bind(DataSourceType.class).annotatedWith(Names.named("oracle")).to(Oracle.class);
bind(DataSourceType.class).annotatedWith(Names.named("postgresql")).to(PostgreSQL.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java
new file mode 100644
index 0000000..bb4c477
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static com.google.gerrit.server.schema.JdbcUtil.hostname;
+import static com.google.gerrit.server.schema.JdbcUtil.port;
+
+import com.google.gerrit.server.config.ConfigSection;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+public class Oracle extends BaseDataSourceType {
+ private Config cfg;
+
+ @Inject
+ public Oracle(@GerritServerConfig final Config cfg) {
+ super("oracle.jdbc.driver.OracleDriver");
+ this.cfg = cfg;
+ }
+
+ @Override
+ public String getUrl() {
+ final StringBuilder b = new StringBuilder();
+ final ConfigSection dbc = new ConfigSection(cfg, "database");
+ b.append("jdbc:oracle:thin:@");
+ b.append(hostname(dbc.optional("hostname")));
+ b.append(port(dbc.optional("port")));
+ b.append(":");
+ b.append(dbc.required("instance"));
+ return b.toString();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
index 305fb84..0cea4bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
@@ -61,7 +61,7 @@
} else {
try {
- u.check(ui, version, db, true);
+ u.check(ui, version, db);
} catch (SQLException e) {
throw new OrmException("Cannot upgrade schema", e);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 53930ac..99bc817 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.schema;
+import com.google.common.collect.Lists;
import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.jdbc.JdbcExecutor;
@@ -25,14 +26,13 @@
import java.sql.SQLException;
import java.sql.Statement;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
- public static final Class<Schema_79> C = Schema_79.class;
+ public static final Class<Schema_83> C = Schema_83.class;
public static class Module extends AbstractModule {
@Override
@@ -68,59 +68,83 @@
return versionNbr;
}
- public final void check(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db, boolean toTargetVersion)
+ public final void check(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db)
throws OrmException, SQLException {
if (curr.versionNbr == versionNbr) {
// Nothing to do, we are at the correct schema.
- //
+ } else if (curr.versionNbr > versionNbr) {
+ throw new OrmException("Cannot downgrade database schema from version "
+ + curr.versionNbr + " to " + versionNbr + ".");
} else {
- upgradeFrom(ui, curr, db, toTargetVersion);
+ upgradeFrom(ui, curr, db);
}
}
/** Runs check on the prior schema version, and then upgrades. */
- protected void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db, boolean toTargetVersion)
+ private void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr, ReviewDb db)
throws OrmException, SQLException {
- final JdbcSchema s = (JdbcSchema) db;
+ List<SchemaVersion> pending = pending(curr.versionNbr);
+ updateSchema(pending, ui, db);
+ migrateData(pending, ui, curr, db);
- if (curr.versionNbr > versionNbr) {
- throw new OrmException("Cannot downgrade database schema from version " + curr.versionNbr
- + " to " + versionNbr + ".");
- }
-
- prior.get().check(ui, curr, db, false);
-
- ui.message("Upgrading database schema from version " + curr.versionNbr
- + " to " + versionNbr + " ...");
-
- preUpdateSchema(db);
- final JdbcExecutor e = new JdbcExecutor(s);
+ JdbcSchema s = (JdbcSchema) db;
+ JdbcExecutor e = new JdbcExecutor(s);
try {
- s.updateSchema(e);
- migrateData(db, ui);
-
- if (toTargetVersion) {
- final List<String> pruneList = new ArrayList<String>();
- s.pruneSchema(new StatementExecutor() {
- public void execute(String sql) {
- pruneList.add(sql);
- }
- });
-
- if (!pruneList.isEmpty()) {
- ui.pruneSchema(e, pruneList);
+ final List<String> pruneList = Lists.newArrayList();
+ s.pruneSchema(new StatementExecutor() {
+ public void execute(String sql) {
+ pruneList.add(sql);
}
+ });
+
+ if (!pruneList.isEmpty()) {
+ ui.pruneSchema(e, pruneList);
}
} finally {
e.close();
}
- finish(curr, db);
+ }
+
+ private List<SchemaVersion> pending(int curr) {
+ List<SchemaVersion> r = Lists.newArrayListWithCapacity(versionNbr - curr);
+ for (SchemaVersion v = this; curr < v.getVersionNbr(); v = v.prior.get()) {
+ r.add(v);
+ }
+ Collections.reverse(r);
+ return r;
+ }
+
+ private void updateSchema(List<SchemaVersion> pending, UpdateUI ui,
+ ReviewDb db) throws OrmException, SQLException {
+ for (SchemaVersion v : pending) {
+ ui.message(String.format("Upgrading schema to %d ...", v.getVersionNbr()));
+ v.preUpdateSchema(db);
+ }
+
+ JdbcSchema s = (JdbcSchema) db;
+ JdbcExecutor e = new JdbcExecutor(s);
+ try {
+ s.updateSchema(e);
+ } finally {
+ e.close();
+ }
}
/** Invoke before updateSchema adds new columns/tables. */
protected void preUpdateSchema(ReviewDb db) throws OrmException, SQLException {
}
+ private void migrateData(List<SchemaVersion> pending, UpdateUI ui,
+ CurrentSchemaVersion curr, ReviewDb db) throws OrmException, SQLException {
+ for (SchemaVersion v : pending) {
+ ui.message(String.format(
+ "Migrating data to schema %d ...",
+ v.getVersionNbr()));
+ v.migrateData(db, ui);
+ v.finish(curr, db);
+ }
+ }
+
/**
* Invoked between updateSchema (adds new columns/tables) and pruneSchema
* (removes deleted columns/tables).
@@ -135,6 +159,18 @@
db.schemaVersion().update(Collections.singleton(curr));
}
+ /** Rename an existing table. */
+ protected void renameTable(ReviewDb db, String from, String to)
+ throws OrmException {
+ final JdbcSchema s = (JdbcSchema) db;
+ final JdbcExecutor e = new JdbcExecutor(s);
+ try {
+ s.renameTable(e, from, to);
+ } finally {
+ e.close();
+ }
+ }
+
/** Rename an existing column. */
protected void renameColumn(ReviewDb db, String table, String from, String to)
throws OrmException {
@@ -147,7 +183,7 @@
}
}
- /** Execute a SQL statement. */
+ /** Execute an SQL statement. */
protected void execute(ReviewDb db, String sql) throws SQLException {
Statement s = ((JdbcSchema) db).getConnection().createStatement();
try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
index 12a22f9..76dbdf5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
@@ -14,9 +14,6 @@
package com.google.gerrit.server.schema;
-import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
@@ -30,12 +27,4 @@
}
});
}
-
- @Override
- protected void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr,
- ReviewDb db, boolean toTargetVersion) throws OrmException {
- throw new OrmException("Cannot upgrade from schema " + curr.versionNbr
- + "; manually run init from Gerrit Code Review 2.1.7"
- + " and restart this version to continue.");
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java
index 94f5d2c..9ee2c6f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java
@@ -14,33 +14,13 @@
package com.google.gerrit.server.schema;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import java.sql.SQLException;
-import java.sql.Statement;
-
public class Schema_66 extends SchemaVersion {
@Inject
Schema_66(Provider<Schema_65> prior) {
super(prior);
}
-
- @Override
- protected void migrateData(ReviewDb db, UpdateUI ui)
- throws OrmException, SQLException {
- final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
- try {
- stmt.executeUpdate("UPDATE accounts SET reverse_patch_set_order = 'Y' "+
- "WHERE display_patch_sets_in_reverse_order = 'Y'");
- stmt.executeUpdate("UPDATE accounts SET show_username_in_review_category = 'Y' " +
- "WHERE display_person_name_in_review_category = 'Y'");
- } finally {
- stmt.close();
- }
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java
index ca012d1..f884c73 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java
@@ -16,8 +16,8 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
@@ -51,10 +51,10 @@
// Initialize some variables
Connection conn = ((JdbcSchema) db).getConnection();
- ArrayList<AccountGroupIncludeByUuid> newIncludes =
- new ArrayList<AccountGroupIncludeByUuid>();
- ArrayList<AccountGroupIncludeByUuidAudit> newIncludeAudits =
- new ArrayList<AccountGroupIncludeByUuidAudit>();
+ ArrayList<AccountGroupById> newIncludes =
+ new ArrayList<AccountGroupById>();
+ ArrayList<AccountGroupByIdAud> newIncludeAudits =
+ new ArrayList<AccountGroupByIdAud>();
// Iterate over all entries in account_group_includes
Statement oldGroupIncludesStmt = conn.createStatement();
@@ -75,8 +75,8 @@
}
// Create the new include entry
- AccountGroupIncludeByUuid destIncludeEntry = new AccountGroupIncludeByUuid(
- new AccountGroupIncludeByUuid.Key(oldGroupId, uuidFromIncludeId));
+ AccountGroupById destIncludeEntry = new AccountGroupById(
+ new AccountGroupById.Key(oldGroupId, uuidFromIncludeId));
// Iterate over all the audits (for this group)
PreparedStatement oldAuditsQuery = conn.prepareStatement(
@@ -89,8 +89,8 @@
int removedBy = oldGroupIncludeAudits.getInt("removed_by");
// Create the new audit entry
- AccountGroupIncludeByUuidAudit destAuditEntry =
- new AccountGroupIncludeByUuidAudit(destIncludeEntry, addedBy,
+ AccountGroupByIdAud destAuditEntry =
+ new AccountGroupByIdAud(destIncludeEntry, addedBy,
oldGroupIncludeAudits.getTimestamp("added_on"));
// If this was a "removed on" entry, note that
@@ -108,7 +108,7 @@
oldGroupIncludesStmt.close();
// Now insert all of the new entries to the database
- db.accountGroupIncludesByUuid().insert(newIncludes);
- db.accountGroupIncludesByUuidAudit().insert(newIncludeAudits);
+ db.accountGroupById().insert(newIncludes);
+ db.accountGroupByIdAud().insert(newIncludeAudits);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_80.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_80.java
new file mode 100644
index 0000000..2cf8d2a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_80.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_80 extends SchemaVersion {
+
+ @Inject
+ Schema_80(Provider<Schema_79> prior) {
+ super(prior);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_81.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_81.java
new file mode 100644
index 0000000..bc3b390
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_81.java
@@ -0,0 +1,159 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.sql.SQLException;
+
+public class Schema_81 extends SchemaVersion {
+
+ private final File pluginsDir;
+ private final GitRepositoryManager mgr;
+ private final AllProjectsName allProjects;
+ private final PersonIdent serverUser;
+
+ @Inject
+ Schema_81(Provider<Schema_80> prior, SitePaths sitePaths,
+ AllProjectsName allProjects, GitRepositoryManager mgr,
+ @GerritPersonIdent PersonIdent serverUser) {
+ super(prior);
+ this.pluginsDir = sitePaths.plugins_dir;
+ this.mgr = mgr;
+ this.allProjects = allProjects;
+ this.serverUser = serverUser;
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
+ SQLException {
+ try {
+ migrateStartReplicationCapability(db, scanForReplicationPlugin());
+ } catch (RepositoryNotFoundException e) {
+ throw new OrmException(e);
+ } catch (SQLException e) {
+ throw new OrmException(e);
+ } catch (IOException e) {
+ throw new OrmException(e);
+ } catch (ConfigInvalidException e) {
+ throw new OrmException(e);
+ }
+ }
+
+ private File[] scanForReplicationPlugin() {
+ File[] matches = null;
+ if (pluginsDir != null && pluginsDir.exists()) {
+ matches = pluginsDir.listFiles(new FileFilter() {
+ @Override
+ public boolean accept(File pathname) {
+ String n = pathname.getName();
+ return (n.endsWith(".jar") || n.endsWith(".jar.disabled"))
+ && pathname.isFile() && n.indexOf("replication") >= 0;
+ }
+ });
+ }
+ return matches;
+ }
+
+ private void migrateStartReplicationCapability(ReviewDb db, File[] matches)
+ throws SQLException, RepositoryNotFoundException, IOException,
+ ConfigInvalidException {
+ Description d = new Description();
+ if (matches == null || matches.length == 0) {
+ d.what = Description.Action.REMOVE;
+ } else {
+ d.what = Description.Action.RENAME;
+ d.prefix = nameOf(matches[0]);
+ }
+ migrateStartReplicationCapability(db, d);
+ }
+
+ private void migrateStartReplicationCapability(ReviewDb db, Description d)
+ throws SQLException, RepositoryNotFoundException, IOException,
+ ConfigInvalidException {
+ Repository git = mgr.openRepository(allProjects);
+ try {
+ MetaDataUpdate md =
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
+ md.getCommitBuilder().setAuthor(serverUser);
+ md.getCommitBuilder().setCommitter(serverUser);
+ ProjectConfig config = ProjectConfig.read(md);
+ AccessSection capabilities =
+ config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES);
+ Permission startReplication =
+ capabilities.getPermission("startReplication");
+ if (startReplication == null) {
+ return;
+ }
+ String msg = null;
+ switch (d.what) {
+ case REMOVE:
+ capabilities.remove(startReplication);
+ msg = "Remove startReplication capability, plugin not installed\n";
+ break;
+ case RENAME:
+ capabilities.remove(startReplication);
+ Permission pluginStartReplication =
+ capabilities.getPermission(
+ String.format("%s-startReplication", d.prefix), true);
+ pluginStartReplication.setRules(startReplication.getRules());
+ msg = "Rename startReplication capability to match updated plugin\n";
+ break;
+ }
+ config.replace(capabilities);
+ md.setMessage(msg);
+ config.commit(md);
+ } finally {
+ git.close();
+ }
+ }
+
+ private static String nameOf(File jar) {
+ String name = jar.getName();
+ if (name.endsWith(".disabled")) {
+ name = name.substring(0, name.lastIndexOf('.'));
+ }
+ int ext = name.lastIndexOf('.');
+ return 0 < ext ? name.substring(0, ext) : name;
+ }
+
+ private static class Description {
+ private enum Action {
+ REMOVE, RENAME
+ }
+ Action what;
+ String prefix;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_82.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_82.java
new file mode 100644
index 0000000..939afe0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_82.java
@@ -0,0 +1,152 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcExecutor;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.DialectMySQL;
+import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Map;
+import java.util.Set;
+
+public class Schema_82 extends SchemaVersion {
+
+ private Map<String,String> tables = ImmutableMap.of(
+ "account_group_includes_by_uuid", "account_group_by_id",
+ "account_group_includes_by_uuid_audit", "account_group_by_id_aud");
+
+ private Map<String,Index> indexes = ImmutableMap.of(
+ "account_project_watches_byProject",
+ new Index("account_project_watches", "account_project_watches_byP"),
+ "patch_set_approvals_closedByUser",
+ new Index("patch_set_approvals", "patch_set_approvals_closedByU"),
+ "submodule_subscription_access_bySubscription",
+ new Index("submodule_subscriptions", "submodule_subscr_acc_byS")
+ );
+
+ @Inject
+ Schema_82(Provider<Schema_81> prior) {
+ super(prior);
+ }
+
+ @Override
+ protected void preUpdateSchema(ReviewDb db) throws OrmException, SQLException {
+ final JdbcSchema s = (JdbcSchema) db;
+ final JdbcExecutor e = new JdbcExecutor(s);
+ renameTables(db, s, e);
+ renameColumn(db, s, e);
+ renameIndexes(db);
+ }
+
+ private void renameTables(final ReviewDb db, final JdbcSchema s,
+ final JdbcExecutor e) throws OrmException, SQLException {
+ SqlDialect dialect = ((JdbcSchema) db).getDialect();
+ final Set<String> existingTables = dialect.listTables(s.getConnection());
+ for (Map.Entry<String, String> entry : tables.entrySet()) {
+ // Does source table exist?
+ if (existingTables.contains(entry.getKey())) {
+ // Does target table exist?
+ if (!existingTables.contains(entry.getValue())) {
+ s.renameTable(e, entry.getKey(), entry.getValue());
+ }
+ }
+ }
+ }
+
+ private void renameColumn(final ReviewDb db, final JdbcSchema s,
+ final JdbcExecutor e) throws SQLException, OrmException {
+ SqlDialect dialect = ((JdbcSchema) db).getDialect();
+ final Set<String> existingColumns =
+ dialect.listColumns(s.getConnection(), "accounts");
+ // Does source column exist?
+ if (!existingColumns.contains("show_username_in_review_category")) {
+ return;
+ }
+ // Does target column exist?
+ if (existingColumns.contains("show_user_in_review")) {
+ return;
+ }
+ s.renameColumn(e, "accounts", "show_username_in_review_category",
+ "show_user_in_review");
+ // MySQL loose check constraint during the column renaming.
+ // Well it doesn't implemented anyway,
+ // check constraints are get parsed but do nothing
+ if (dialect instanceof DialectMySQL) {
+ Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ addCheckConstraint(stmt);
+ } finally {
+ stmt.close();
+ }
+ }
+ }
+
+ private void renameIndexes(ReviewDb db) throws SQLException {
+ SqlDialect dialect = ((JdbcSchema) db).getDialect();
+ Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ // MySQL doesn't have alter index stmt, drop & create
+ if (dialect instanceof DialectMySQL) {
+ for (Map.Entry<String, Index> entry : indexes.entrySet()) {
+ stmt.executeUpdate("DROP INDEX " + entry.getKey() + " ON "
+ + entry.getValue().table);
+ }
+ stmt.executeUpdate("CREATE INDEX account_project_watches_byP ON " +
+ "account_project_watches (project_name)");
+ stmt.executeUpdate("CREATE INDEX patch_set_approvals_closedByU ON " +
+ "patch_set_approvals (change_open, account_id, change_sort_key)");
+ stmt.executeUpdate("CREATE INDEX submodule_subscr_acc_bys ON " +
+ "submodule_subscriptions (submodule_project_name, " +
+ "submodule_branch_name)");
+ } else {
+ for (Map.Entry<String, Index> entry : indexes.entrySet()) {
+ stmt.executeUpdate("ALTER INDEX " + entry.getKey() + " RENAME TO "
+ + entry.getValue().index);
+ }
+ }
+ } catch (SQLException e) {
+ // we don't care
+ // better we would check if index was already renamed
+ // gwtorm doesn't expose this functionality
+ } finally {
+ stmt.close();
+ }
+ }
+
+ private void addCheckConstraint(Statement stmt) throws SQLException {
+ // add check constraint for the destination column
+ stmt.executeUpdate("ALTER TABLE accounts ADD CONSTRAINT "
+ + "show_user_in_review_check CHECK "
+ + "(show_user_in_review IN('Y', 'N'))");
+ }
+
+ static class Index {
+ String table;
+ String index;
+
+ Index(String tableName, String indexName) {
+ this.table = tableName;
+ this.index = indexName;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_83.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_83.java
new file mode 100644
index 0000000..160d41c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_83.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_83 extends SchemaVersion {
+
+ @Inject
+ Schema_83(Provider<Schema_82> prior) {
+ super(prior);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
index b5aead8..254a780 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
@@ -14,8 +14,10 @@
package com.google.gerrit.server.schema;
+import com.google.common.base.CharMatcher;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.SqlDialect;
import com.google.gwtorm.server.OrmException;
import java.io.BufferedReader;
@@ -28,7 +30,7 @@
import java.util.ArrayList;
import java.util.List;
-/** Parses a SQL script from a resource file and later runs it. */
+/** Parses an SQL script from a resource file and later runs it. */
class ScriptRunner {
private final String name;
private final List<String> commands;
@@ -49,11 +51,16 @@
void run(final ReviewDb db) throws OrmException {
try {
- final Connection c = ((JdbcSchema) db).getConnection();
+ final JdbcSchema schema = (JdbcSchema)db;
+ final Connection c = schema.getConnection();
+ final SqlDialect dialect = schema.getDialect();
final Statement stmt = c.createStatement();
try {
for (String sql : commands) {
try {
+ if (!dialect.isStatementDelimiterSupported()) {
+ sql = CharMatcher.is(';').trimTrailingFrom(sql);
+ }
stmt.execute(sql);
} catch (SQLException e) {
throw new OrmException("Error in " + name + ":\n" + sql, e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java
new file mode 100644
index 0000000..6730e30
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ServerRequestContext.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.InternalUser;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.Inject;
+
+/** RequestContext with an InternalUser making the internals visible. */
+public class ServerRequestContext implements RequestContext {
+ private final InternalUser user;
+
+ @Inject
+ ServerRequestContext(InternalUser.Factory userFactory) {
+ this.user = userFactory.create();
+ }
+
+ @Override
+ public CurrentUser getCurrentUser() {
+ return user;
+ }
+
+ @Override
+ public Provider<ReviewDb> getReviewDbProvider() {
+ return new Provider<ReviewDb>() {
+ @Override
+ public ReviewDb get() {
+ throw new ProvisionException(
+ "Automatic ReviewDb only available in request scope");
+ }
+ };
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
index bd43e9d..2aa07b48 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -59,7 +59,7 @@
@Provides
IdentifiedUser provideCurrentUser(CurrentUser user) {
- if (user instanceof IdentifiedUser) {
+ if (user.isIdentifiedUser()) {
return (IdentifiedUser) user;
}
throw new ProvisionException(NotSignedInException.MESSAGE,
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
index 11911bd..dd2a008 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
@@ -51,7 +51,7 @@
CurrentUser curUser = cControl.getCurrentUser();
Term resultTerm;
- if (curUser instanceof IdentifiedUser) {
+ if (curUser.isIdentifiedUser()) {
Account.Id id = ((IdentifiedUser)curUser).getAccountId();
resultTerm = new IntegerTerm(id.get());
} else if (curUser instanceof AnonymousUser) {
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
index 3f4b656..62fe075 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
@@ -112,7 +112,6 @@
}
private static IdentifiedUser.GenericFactory userFactory(Prolog engine) {
- PrologEnvironment env = (PrologEnvironment) engine.control;
- return env.getInjector().getInstance(IdentifiedUser.GenericFactory.class);
+ return ((PrologEnvironment) engine.control).getArgs().getUserFactory();
}
}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
index 161da79..698c11c 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
@@ -15,10 +15,8 @@
package gerrit;
import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.rules.PrologEnvironment;
+import com.google.gerrit.common.data.LabelValue;
import com.google.gerrit.rules.StoredValues;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
import com.googlecode.prolog_cafe.lang.IntegerTerm;
import com.googlecode.prolog_cafe.lang.ListTerm;
@@ -44,6 +42,8 @@
* </ul>
*/
class PRED_get_legacy_label_types_1 extends Predicate.P1 {
+ private static final SymbolTerm NONE = SymbolTerm.intern("none");
+
PRED_get_legacy_label_types_1(Term a1, Operation n) {
arg1 = a1;
cont = n;
@@ -53,14 +53,8 @@
public Operation exec(Prolog engine) throws PrologException {
engine.setB0();
Term a1 = arg1.dereference();
-
- PrologEnvironment env = (PrologEnvironment) engine.control;
- ProjectState state = env.getInjector().getInstance(ProjectCache.class)
- .get(StoredValues.CHANGE.get(engine).getDest().getParentKey());
- if (state == null) {
- return engine.fail();
- }
- List<LabelType> list = state.getLabelTypes().getLabelTypes();
+ List<LabelType> list =
+ StoredValues.CHANGE_CONTROL.get(engine).getLabelTypes().getLabelTypes();
Term head = Prolog.Nil;
for (int idx = list.size() - 1; 0 <= idx; idx--) {
head = new ListTerm(export(list.get(idx)), head);
@@ -76,10 +70,12 @@
"label_type", 4);
static Term export(LabelType type) {
+ LabelValue min = type.getMin();
+ LabelValue max = type.getMax();
return new StructureTerm(symLabelType,
SymbolTerm.intern(type.getName()),
SymbolTerm.intern(type.getFunctionName()),
- new IntegerTerm(type.getMin().getValue()),
- new IntegerTerm(type.getMax().getValue()));
+ min != null ? new IntegerTerm(min.getValue()) : NONE,
+ max != null ? new IntegerTerm(max.getValue()) : NONE);
}
}
diff --git a/gerrit-server/src/main/prolog/BUCK b/gerrit-server/src/main/prolog/BUCK
new file mode 100644
index 0000000..09a6553
--- /dev/null
+++ b/gerrit-server/src/main/prolog/BUCK
@@ -0,0 +1,8 @@
+include_defs('//lib/prolog/prolog.defs')
+
+prolog_cafe_library(
+ name = 'common',
+ srcs = ['gerrit_common.pl'],
+ deps = ['//gerrit-server:server'],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
index 71e7383..4738d15 100644
--- a/gerrit-server/src/main/prolog/gerrit_common.pl
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -239,6 +239,7 @@
%% Apply the old -2..+2 style logic.
%%
legacy_submit_rule('MaxWithBlock', Label, Min, Max, T) :- !, max_with_block(Label, Min, Max, T).
+legacy_submit_rule('AnyWithBlock', Label, Min, Max, T) :- !, any_with_block(Label, Min, T).
legacy_submit_rule('MaxNoBlock', Label, Min, Max, T) :- !, max_no_block(Label, Max, T).
legacy_submit_rule('NoBlock', Label, Min, Max, T) :- !, T = may(_).
legacy_submit_rule('NoOp', Label, Min, Max, T) :- !, T = may(_).
@@ -267,6 +268,7 @@
max_with_block(Label, Min, Max, need(Max)) :-
true
.
+
%TODO Uncomment this clause when group suggesting is possible.
%max_with_block(Label, Min, Max, need(Max, Group)) :-
% \+ check_label_range_permission(Label, Max, ok(_)),
@@ -276,6 +278,16 @@
% \+ check_label_range_permission(Label, Max, ask(Group))
% .
+%% any_with_block:
+%%
+%% - The maximum is never used.
+%%
+any_with_block(Label, Min, reject(Who)) :-
+ check_label_range_permission(Label, Min, ok(Who)),
+ !
+ .
+any_with_block(Label, Min, may(_)).
+
%% max_no_block:
%%
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
new file mode 100644
index 0000000..a1b966f7
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/config/CapabilityConstants.properties
@@ -0,0 +1,16 @@
+accessDatabase = Access Database
+administrateServer = Administrate Server
+createAccount = Create Account
+createGroup = Create Group
+createProject = Create Project
+emailReviewers = Email Reviewers
+flushCaches = Flush Caches
+killTask = Kill Task
+priority = Priority
+queryLimit = Query Limit
+runAs = Run As
+runGC = Run Garbage Collection
+streamEvents = Stream Events
+viewCaches = View Caches
+viewConnections = View Connections
+viewQueue = View Queue
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm
new file mode 100644
index 0000000..28f29fd
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm
@@ -0,0 +1,33 @@
+## Copyright (C) 2013 The Android Open Source Project
+##
+## Licensed under the Apache License, Version 2.0 (the "License");
+## you may not use this file except in compliance with the License.
+## You may obtain a copy of the License at
+##
+## http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+##
+##
+## Template Type:
+## -------------
+## This is a velocity mail template, see: http://velocity.apache.org and the
+## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates.
+##
+## Template File Names and extensions:
+## ----------------------------------
+## Gerrit will use templates ending in ".vm" but will ignore templates ending
+## in ".vm.example". If a .vm template does not exist, the default internal
+## gerrit template which is the same as the .vm.example will be used. If you
+## want to override the default template, copy the .vm.example file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The Footer.vm template will determine the contents of the footer text
+## appended to the end of all outgoing emails after the ChangeFooter and
+## CommentFooter.
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
index 42f2ca9..d524b48 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
@@ -46,7 +46,6 @@
#if($email.changeUrl)
$email.changeUrl
-
#end
#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RebasedPatchSet.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RebasedPatchSet.vm
deleted file mode 100644
index e761627..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RebasedPatchSet.vm
+++ /dev/null
@@ -1,54 +0,0 @@
-## Copyright (C) 2012 The Android Open Source Project
-##
-## Licensed under the Apache License, Version 2.0 (the "License");
-## you may not use this file except in compliance with the License.
-## You may obtain a copy of the License at
-##
-## http://www.apache.org/licenses/LICENSE-2.0
-##
-## Unless required by applicable law or agreed to in writing, software
-## distributed under the License is distributed on an "AS IS" BASIS,
-## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-## See the License for the specific language governing permissions and
-## limitations under the License.
-##
-##
-## Template Type:
-## -------------
-## This is a velocity mail template, see: http://velocity.apache.org and the
-## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates.
-##
-## Template File Names and extensions:
-## ----------------------------------
-## Gerrit will use templates ending in ".vm" but will ignore templates ending
-## in ".vm.example". If a .vm template does not exist, the default internal
-## gerrit template which is the same as the .vm.example will be used. If you
-## want to override the default template, copy the .vm.example file to a .vm
-## file and edit it appropriately.
-##
-## This Template:
-## --------------
-## The RebasedPatchSet.vm template will determine the contents of the email
-## related to a user rebasing a patchset for a change through the Gerrit UI.
-## It is a ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm.
-##
-#if($email.reviewerNames)
-Hello $email.joinStrings($email.reviewerNames, ', '),
-
-I'd like you to reexamine a rebased change.#if($email.changeUrl) Please visit
-
- $email.changeUrl
-
-to look at the new rebased patch set (#$patchSet.patchSetId).
-#end
-#else
-$fromName has created a new patch set by issuing a rebase in Gerrit (#$patchSet.patchSetId).
-#end
-
-Change subject: $change.subject
-......................................................................
-
-$email.changeDetail
-#if($email.sshHost)
- git pull ssh://$email.sshHost/$projectName $patchSet.refName
-#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index b29e25c..12dcd52 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -38,6 +38,11 @@
return
fi
+ if test "false" = "`git config --bool --get gerrit.createChangeId`"
+ then
+ return
+ fi
+
# Does Change-Id: already exist? if so, exit (no change).
if grep -i '^Change-Id:' "$MSG" >/dev/null
then
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
index 9b122eb..6c605d2 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
@@ -49,7 +49,15 @@
load("gerrit", "gerrit_common_test.pl", new AbstractModule() {
@Override
protected void configure() {
- bind(ProjectCache.class).toInstance(projects);
+ bind(PrologEnvironment.Args.class).toInstance(
+ new PrologEnvironment.Args(
+ projects,
+ null,
+ null,
+ null,
+ null,
+ null
+ ));
}
});
}
@@ -61,6 +69,10 @@
new Branch.NameKey(projects.allProjectsName, "master")));
}
+ public void testGerritCommon() {
+ runPrologBasedTests();
+ }
+
private static LabelValue value(int value, String text) {
return new LabelValue((short) value, text);
}
@@ -96,11 +108,21 @@
}
@Override
+ public ProjectState checkedGet(Project.NameKey projectName) {
+ return get(projectName);
+ }
+
+ @Override
public void evict(Project p) {
throw new UnsupportedOperationException();
}
@Override
+ public void evict(Project.NameKey p) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
public void remove(Project p) {
throw new UnsupportedOperationException();
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
index 83809a4..4d86596a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
@@ -55,7 +55,7 @@
protected void load(String pkg, String prologResource, Module... modules)
throws CompileException, IOException {
ArrayList<Module> moduleList = new ArrayList<Module>();
- moduleList.add(new PrologModule());
+ moduleList.add(new PrologModule.EnvironmentModule());
moduleList.addAll(Arrays.asList(modules));
envFactory = Guice.createInjector(moduleList)
@@ -114,7 +114,7 @@
return env.execute(Prolog.BUILTIN, "clause", head, new VariableTerm());
}
- public void testRunPrologTestCases() {
+ public void runPrologBasedTests() {
int errors = 0;
long start = System.currentTimeMillis();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/ChangeJsonTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/ChangeJsonTest.java
deleted file mode 100644
index 14e0ebf..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/ChangeJsonTest.java
+++ /dev/null
@@ -1,233 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.change;
-
-import static org.easymock.EasyMock.anyBoolean;
-import static org.easymock.EasyMock.anyObject;
-import static org.easymock.EasyMock.createMock;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.expectLastCall;
-import static org.easymock.EasyMock.replay;
-
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
-import com.google.gerrit.common.changes.ListChangesOption;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ChangeAccess;
-import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
-import com.google.gerrit.reviewdb.server.PatchSetAccess;
-import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.AccountByEmailCache;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountInfo;
-import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
-import com.google.gerrit.server.change.ChangeJson.ChangeMessageInfo;
-import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.ListResultSet;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import com.google.inject.Binder;
-import com.google.inject.Guice;
-import com.google.inject.Module;
-
-import junit.framework.TestCase;
-
-import org.easymock.EasyMock;
-import org.easymock.IAnswer;
-import org.eclipse.jgit.lib.Config;
-
-import java.sql.Timestamp;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-
-public class ChangeJsonTest extends TestCase {
-
- public void testFormatChangeMessages() throws OrmException {
-
- // create mocks
- final CurrentUser currentUser = createMock(CurrentUser.class);
- final GitRepositoryManager grm = createMock(GitRepositoryManager.class);
- final AccountByEmailCache abec = createMock(AccountByEmailCache.class);
- final AccountCache ac = createMock(AccountCache.class);
- final AccountInfo.Loader.Factory alf =
- createMock(AccountInfo.Loader.Factory.class);
- final CapabilityControl.Factory ccf =
- createMock(CapabilityControl.Factory.class);
- final GroupBackend gb = createMock(GroupBackend.class);
- final Realm r = createMock(Realm.class);
- final PatchListCache plc = createMock(PatchListCache.class);
- final ProjectCache pc = createMock(ProjectCache.class);
- final Config config = new Config(); // unable to mock
- final ReviewDb rdb = createMock(ReviewDb.class);
- final ChangeAccess ca = createMock(ChangeAccess.class);
- final PatchSetAccess psa = createMock(PatchSetAccess.class);
- final PatchSetApprovalAccess psaa =
- createMock(PatchSetApprovalAccess.class);
- final ChangeMessageAccess cma = createMock(ChangeMessageAccess.class);
- AccountInfo.Loader accountLoader = createMock(AccountInfo.Loader.class);
-
- // create ChangeJson instance
- Module mod = new Module() {
- @Override
- public void configure(Binder binder) {
- binder.bind(CurrentUser.class).toInstance(currentUser);
- binder.bind(GitRepositoryManager.class).toInstance(grm);
- binder.bind(AccountByEmailCache.class).toInstance(abec);
- binder.bind(AccountCache.class).toInstance(ac);
- binder.bind(AccountInfo.Loader.Factory.class).toInstance(alf);
- binder.bind(CapabilityControl.Factory.class).toInstance(ccf);
- binder.bind(GroupBackend.class).toInstance(gb);
- binder.bind(Realm.class).toInstance(r);
- binder.bind(PatchListCache.class).toInstance(plc);
- binder.bind(ProjectCache.class).toInstance(pc);
- binder.bind(ReviewDb.class).toInstance(rdb);
- binder.bind(Config.class).annotatedWith(GerritServerConfig.class)
- .toInstance(config);
- binder.bind(String.class).annotatedWith(CanonicalWebUrl.class)
- .toInstance("");
- binder.bind(String.class).annotatedWith(AnonymousCowardName.class)
- .toInstance("");
- }
- };
- ChangeJson json = Guice.createInjector(mod).getInstance(ChangeJson.class);
-
- // define mock behavior for tests
- expect(alf.create(anyBoolean())).andReturn(accountLoader).anyTimes();
-
- Project.NameKey proj = new Project.NameKey("ProjectNameKey");
- Branch.NameKey forBranch = new Branch.NameKey(proj, "BranchNameKey");
-
- Change.Key changeKey123 = new Change.Key("ChangeKey123");
- Change.Id changeId123 = new Change.Id(123);
- Change change123 = new Change(changeKey123, changeId123, null, forBranch);
-
- Change.Key changeKey234 = new Change.Key("ChangeKey234");
- Change.Id changeId234 = new Change.Id(234);
- Change change234 = new Change(changeKey234, changeId234, null, forBranch);
-
- expect(ca.get(Sets.newHashSet(changeId123)))
- .andAnswer(results(Change.class, change123)).anyTimes();
- expect(ca.get(changeId123)).andReturn(change123).anyTimes();
- expect(ca.get(Sets.newHashSet(changeId234)))
- .andAnswer(results(Change.class, change234));
- expect(ca.get(changeId234)).andReturn(change234);
- expect(rdb.changes()).andReturn(ca).anyTimes();
-
- expect(psa.get(EasyMock.<Iterable<PatchSet.Id>>anyObject()))
- .andAnswer(results(PatchSet.class)).anyTimes();
- expect(rdb.patchSets()).andReturn(psa).anyTimes();
-
- expect(psaa.byPatchSet(anyObject(PatchSet.Id.class)))
- .andAnswer(results(PatchSetApproval.class)).anyTimes();
- expect(rdb.patchSetApprovals()).andReturn(psaa).anyTimes();
-
- expect(currentUser.getStarredChanges())
- .andReturn(Collections.<Change.Id>emptySet()).anyTimes();
-
- long timeBase = System.currentTimeMillis();
- ChangeMessage changeMessage1 =changeMessage(
- changeId123, "cm1", 111, timeBase, 1111, "first message");
- ChangeMessage changeMessage2 = changeMessage(
- changeId123, "cm2", 222, timeBase + 1000, 1111, "second message");
- expect(cma.byChange(changeId123))
- .andAnswer(results(ChangeMessage.class, changeMessage2, changeMessage1))
- .anyTimes();
- expect(cma.byChange(changeId234)).andAnswer(results(ChangeMessage.class));
- expect(rdb.changeMessages()).andReturn(cma).anyTimes();
-
- expect(accountLoader.get(anyObject(Account.Id.class)))
- .andAnswer(accountForId()).anyTimes();
- accountLoader.fill();
- expectLastCall().anyTimes();
-
- replay(rdb, ca, psa, psaa, alf, currentUser, cma, accountLoader);
-
- // test 1: messages not returned by default
- ChangeInfo ci = json.format(new ChangeData(changeId123));
- assertNull(ci.messages);
-
- json.addOption(ListChangesOption.MESSAGES);
-
- // test 2: two change messages, in chronological order
- ci = json.format(new ChangeData(changeId123));
- assertNotNull(ci.messages);
- assertEquals(2, ci.messages.size());
- Iterator<ChangeMessageInfo> cmis = ci.messages.iterator();
- assertEquals(changeMessage1, cmis.next());
- assertEquals(changeMessage2, cmis.next());
-
- // test 3: no change messages
- ci = json.format(new ChangeData(changeId234));
- assertNotNull(ci.messages);
- assertEquals(0, ci.messages.size());
- }
-
- private static IAnswer<AccountInfo> accountForId() {
- return new IAnswer<AccountInfo>() {
- @Override
- public AccountInfo answer() throws Throwable {
- Account.Id id = (Account.Id) EasyMock.getCurrentArguments()[0];
- AccountInfo ai = new AccountInfo(id);
- return ai;
- }};
- }
-
- private static <T> IAnswer<ResultSet<T>> results(Class<T> type, T... items) {
- final List<T> list = Lists.newArrayList(items);
- return new IAnswer<ResultSet<T>>() {
- @Override
- public ResultSet<T> answer() throws Throwable {
- return new ListResultSet<T>(list);
- }};
- }
-
- private static void assertEquals(ChangeMessage cm, ChangeMessageInfo cmi) {
- assertEquals(cm.getPatchSetId().get(), (int) cmi._revisionNumber);
- assertEquals(cm.getMessage(), cmi.message);
- assertEquals(cm.getKey().get(), cmi.id);
- assertEquals(cm.getWrittenOn(), cmi.date);
- assertNotNull(cmi.author);
- assertEquals(cm.getAuthor(), cmi.author._id);
- }
-
- private static ChangeMessage changeMessage(Change.Id changeId,
- String uuid, int accountId, long time, int psId, String message) {
- ChangeMessage.Key key = new ChangeMessage.Key(changeId, uuid);
- Account.Id author = new Account.Id(accountId);
- Timestamp updated = new Timestamp(time);
- PatchSet.Id ps = new PatchSet.Id(changeId, psId);
- ChangeMessage changeMessage = new ChangeMessage(key, author, updated, ps);
- changeMessage.setMessage(message);
- return changeMessage;
- }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
index 21d1ce4..f1d986d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
@@ -22,6 +22,7 @@
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.changes.Side;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -33,10 +34,10 @@
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.CommentRange;
import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountInfo;
-import com.google.gerrit.server.change.CommentInfo.Side;
import com.google.gwtorm.server.ListResultSet;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.AbstractModule;
@@ -111,13 +112,13 @@
long timeBase = System.currentTimeMillis();
plc1 = newPatchLineComment(psId1, "Comment1", null,
"FileOne.txt", Side.REVISION, 1, account1, timeBase,
- "First Comment");
+ "First Comment", new CommentRange(1, 2, 3, 4));
plc2 = newPatchLineComment(psId1, "Comment2", "Comment1",
"FileOne.txt", Side.REVISION, 1, account2, timeBase + 1000,
- "Reply to First Comment");
+ "Reply to First Comment", new CommentRange(1, 2, 3, 4));
plc3 = newPatchLineComment(psId1, "Comment3", "Comment1",
"FileOne.txt", Side.PARENT, 1, account1, timeBase + 2000,
- "First Parent Comment");
+ "First Parent Comment", new CommentRange(1, 2, 3, 4));
expect(plca.publishedByPatchSet(psId1))
.andAnswer(results(plc1, plc2, plc3)).anyTimes();
@@ -206,17 +207,19 @@
assertEquals(plc.getSide() == 0 ? Side.PARENT : Side.REVISION,
Objects.firstNonNull(ci.side, Side.REVISION));
assertEquals(plc.getWrittenOn(), ci.updated);
+ assertEquals(plc.getRange(), ci.range);
}
private static PatchLineComment newPatchLineComment(PatchSet.Id psId,
String uuid, String inReplyToUuid, String filename, Side side, int line,
- Account.Id authorId, long millis, String message) {
+ Account.Id authorId, long millis, String message, CommentRange range) {
Patch.Key p = new Patch.Key(psId, filename);
PatchLineComment.Key id = new PatchLineComment.Key(p, uuid);
PatchLineComment plc =
new PatchLineComment(id, line, authorId, inReplyToUuid);
plc.setMessage(message);
- plc.setSide(side == CommentInfo.Side.PARENT ? (short) 0 : (short) 1);
+ plc.setRange(range);
+ plc.setSide(side == Side.PARENT ? (short) 0 : (short) 1);
plc.setStatus(Status.PUBLISHED);
plc.setWrittenOn(new Timestamp(millis));
return plc;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java
new file mode 100644
index 0000000..ae58819
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/IncludedInResolverTest.java
@@ -0,0 +1,205 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.data.IncludedInDetail;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class IncludedInResolverTest extends RepositoryTestCase {
+
+ // Branch names
+ private static final String BRANCH_MASTER = "master";
+ private static final String BRANCH_1_0 = "rel-1.0";
+ private static final String BRANCH_1_3 = "rel-1.3";
+ private static final String BRANCH_2_0 = "rel-2.0";
+ private static final String BRANCH_2_5 = "rel-2.5";
+
+ // Tag names
+ private static final String TAG_1_0 = "1.0";
+ private static final String TAG_1_0_1 = "1.0.1";
+ private static final String TAG_1_3 = "1.3";
+ private static final String TAG_2_0_1 = "2.0.1";
+ private static final String TAG_2_0 = "2.0";
+ private static final String TAG_2_5 = "2.5";
+ private static final String TAG_2_5_ANNOTATED = "2.5-annotated";
+ private static final String TAG_2_5_ANNOTATED_TWICE = "2.5-annotated_twice";
+
+ // Commits
+ private RevCommit commit_initial;
+ private RevCommit commit_v1_3;
+ private RevCommit commit_v2_5;
+
+ private List<String> expTags = new ArrayList<String>();
+ private List<String> expBranches = new ArrayList<String>();
+
+ private RevWalk revWalk;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ /*- The following graph will be created.
+
+ o tag 2.5, 2.5_annotated, 2.5_annotated_twice
+ |\
+ | o tag 2.0.1
+ | o tag 2.0
+ o | tag 1.3
+ |/
+ o c3
+
+ | o tag 1.0.1
+ |/
+ o tag 1.0
+ o c2
+ o c1
+
+ */
+
+ Git git = new Git(db);
+ revWalk = new RevWalk(db);
+ // Version 1.0
+ commit_initial = git.commit().setMessage("c1").call();
+ git.commit().setMessage("c2").call();
+ RevCommit commit_v1_0 = git.commit().setMessage("version 1.0").call();
+ git.tag().setName(TAG_1_0).setObjectId(commit_v1_0).call();
+ RevCommit c3 = git.commit().setMessage("c3").call();
+ // Version 1.01
+ createAndCheckoutBranch(commit_v1_0, BRANCH_1_0);
+ RevCommit commit_v1_0_1 =
+ git.commit().setMessage("verREFS_HEADS_RELsion 1.0.1").call();
+ git.tag().setName(TAG_1_0_1).setObjectId(commit_v1_0_1).call();
+ // Version 1.3
+ createAndCheckoutBranch(c3, BRANCH_1_3);
+ commit_v1_3 = git.commit().setMessage("version 1.3").call();
+ git.tag().setName(TAG_1_3).setObjectId(commit_v1_3).call();
+ // Version 2.0
+ createAndCheckoutBranch(c3, BRANCH_2_0);
+ RevCommit commit_v2_0 = git.commit().setMessage("version 2.0").call();
+ git.tag().setName(TAG_2_0).setObjectId(commit_v2_0).call();
+ RevCommit commit_v2_0_1 = git.commit().setMessage("version 2.0.1").call();
+ git.tag().setName(TAG_2_0_1).setObjectId(commit_v2_0_1).call();
+
+ // Version 2.5
+ createAndCheckoutBranch(commit_v1_3, BRANCH_2_5);
+ git.merge().include(commit_v2_0_1).setCommit(false)
+ .setFastForward(FastForwardMode.NO_FF).call();
+ commit_v2_5 = git.commit().setMessage("version 2.5").call();
+ git.tag().setName(TAG_2_5).setObjectId(commit_v2_5).setAnnotated(false)
+ .call();
+ Ref ref_tag_2_5_annotated =
+ git.tag().setName(TAG_2_5_ANNOTATED).setObjectId(commit_v2_5)
+ .setAnnotated(true).call();
+ RevTag tag_2_5_annotated =
+ revWalk.parseTag(ref_tag_2_5_annotated.getObjectId());
+ git.tag().setName(TAG_2_5_ANNOTATED_TWICE).setObjectId(tag_2_5_annotated)
+ .setAnnotated(true).call();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ revWalk.release();
+ super.tearDown();
+ }
+
+ @Test
+ public void resolveLatestCommit() throws Exception {
+ // Check tip commit
+ IncludedInDetail detail = resolve(commit_v2_5);
+
+ // Check that only tags and branches which refer the tip are returned
+ expTags.add(TAG_2_5);
+ expTags.add(TAG_2_5_ANNOTATED);
+ expTags.add(TAG_2_5_ANNOTATED_TWICE);
+ assertEquals(expTags, detail.getTags());
+ expBranches.add(BRANCH_2_5);
+ assertEquals(expBranches, detail.getBranches());
+ }
+
+ @Test
+ public void resolveFirstCommit() throws Exception {
+ // Check first commit
+ IncludedInDetail detail = resolve(commit_initial);
+
+ // Check whether all tags and branches are returned
+ expTags.add(TAG_1_0);
+ expTags.add(TAG_1_0_1);
+ expTags.add(TAG_1_3);
+ expTags.add(TAG_2_0);
+ expTags.add(TAG_2_0_1);
+ expTags.add(TAG_2_5);
+ expTags.add(TAG_2_5_ANNOTATED);
+ expTags.add(TAG_2_5_ANNOTATED_TWICE);
+ assertEquals(expTags, detail.getTags());
+
+ expBranches.add(BRANCH_MASTER);
+ expBranches.add(BRANCH_1_0);
+ expBranches.add(BRANCH_1_3);
+ expBranches.add(BRANCH_2_0);
+ expBranches.add(BRANCH_2_5);
+ assertEquals(expBranches, detail.getBranches());
+ }
+
+ @Test
+ public void resolveBetwixtCommit() throws Exception {
+ // Check a commit somewhere in the middle
+ IncludedInDetail detail = resolve(commit_v1_3);
+
+ // Check whether all succeeding tags and branches are returned
+ expTags.add(TAG_1_3);
+ expTags.add(TAG_2_5);
+ expTags.add(TAG_2_5_ANNOTATED);
+ expTags.add(TAG_2_5_ANNOTATED_TWICE);
+ assertEquals(expTags, detail.getTags());
+
+ expBranches.add(BRANCH_1_3);
+ expBranches.add(BRANCH_2_5);
+ assertEquals(expBranches, detail.getBranches());
+ }
+
+ private IncludedInDetail resolve(RevCommit commit) throws Exception {
+ return IncludedInResolver.resolve(db, revWalk, commit);
+ }
+
+ private void assertEquals(List<String> list1, List<String> list2) {
+ Collections.sort(list1);
+ Collections.sort(list2);
+ Assert.assertEquals(list1, list2);
+ }
+
+ private void createAndCheckoutBranch(ObjectId objectId, String branchName)
+ throws IOException {
+ String fullBranchName = "refs/heads/" + branchName;
+ super.createBranch(objectId, fullBranchName);
+ super.checkoutBranch(fullBranchName);
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java
new file mode 100644
index 0000000..992502f
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ListCapabilitiesTest.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.server.config.ListCapabilities.CapabilityInfo;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+public class ListCapabilitiesTest {
+ private Injector injector;
+
+ @Before
+ public void setUp() throws Exception {
+ AbstractModule mod = new AbstractModule() {
+ @Override
+ protected void configure() {
+ DynamicMap.mapOf(binder(), CapabilityDefinition.class);
+ bind(CapabilityDefinition.class)
+ .annotatedWith(Exports.named("printHello"))
+ .toInstance(new CapabilityDefinition() {
+ @Override
+ public String getDescription() {
+ return "Print Hello";
+ }
+ });
+ }
+ };
+ injector = Guice.createInjector(mod);
+ }
+
+ @Test
+ public void testList() throws Exception {
+ Map<String, CapabilityInfo> m =
+ injector.getInstance(ListCapabilities.class)
+ .apply(new ConfigResource());
+ for (String id : GlobalCapability.getAllNames()) {
+ assertTrue("contains " + id, m.containsKey(id));
+ assertEquals(id, m.get(id).id);
+ assertNotNull(id + " has name", m.get(id).name);
+ }
+
+ String pluginCapability = "gerrit-printHello";
+ assertTrue("contains " + pluginCapability, m.containsKey(pluginCapability));
+ assertEquals(pluginCapability, m.get(pluginCapability).id);
+ assertEquals("Print Hello", m.get(pluginCapability).name);
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java
index 9a8fc00..0087df6 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java
@@ -21,10 +21,9 @@
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
-import java.util.UUID;
public class SitePathsTest extends TestCase {
- public void testCreate_NotExisting() throws FileNotFoundException {
+ public void testCreate_NotExisting() throws IOException {
final File root = random();
final SitePaths site = new SitePaths(root);
assertTrue(site.isNew);
@@ -32,7 +31,7 @@
assertEquals(new File(root, "etc"), site.etc_dir);
}
- public void testCreate_Empty() throws FileNotFoundException {
+ public void testCreate_Empty() throws IOException {
final File root = random();
try {
assertTrue(root.mkdir());
@@ -91,8 +90,11 @@
assertEquals(new File(pfx + "a").getCanonicalFile(), site.resolve(pfx + "a"));
}
- private File random() {
- final File t = new File("target");
- return new File(t, "random-name-" + UUID.randomUUID().toString());
+ private static File random() throws IOException {
+ File tmp = File.createTempFile("gerrit_test_", "_site");
+ if (!tmp.delete()) {
+ throw new IOException("Cannot create " + tmp.getPath());
+ }
+ return tmp;
}
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
index a849e68..0d5207a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -159,9 +159,8 @@
+ "[contributor-agreement \"Individual\"]\n" //
+ " description = A new description\n" //
+ " accepted = group Staff\n" //
- + " agreementUrl = http://www.example.com/agree\n" //
- + "[project]\n"//
- + "\tstate = active\n", text(rev, "project.config"));
+ + " agreementUrl = http://www.example.com/agree\n",
+ text(rev, "project.config"));
}
@Test
@@ -188,9 +187,7 @@
+ " submit = group People Who Can Submit\n" //
+ "\tsubmit = group Staff\n" //
+ " upload = group Developers\n" //
- + " read = group Developers\n"//
- + "[project]\n"//
- + "\tstate = active\n", text(rev, "project.config"));
+ + " read = group Developers\n", text(rev, "project.config"));
}
private ProjectConfig read(RevCommit rev) throws IOException,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
new file mode 100644
index 0000000..bc96b44
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
@@ -0,0 +1,348 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import static com.google.gerrit.reviewdb.client.Change.Status.ABANDONED;
+import static com.google.gerrit.reviewdb.client.Change.Status.DRAFT;
+import static com.google.gerrit.reviewdb.client.Change.Status.MERGED;
+import static com.google.gerrit.reviewdb.client.Change.Status.NEW;
+import static com.google.gerrit.reviewdb.client.Change.Status.SUBMITTED;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.query.AndPredicate;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.RewritePredicate;
+import com.google.gerrit.server.query.change.AndSource;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.OrSource;
+import com.google.gerrit.server.query.change.SqlRewriterImpl;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+import junit.framework.TestCase;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+@SuppressWarnings("unchecked")
+public class IndexRewriteTest extends TestCase {
+ private static Schema<ChangeData> V1 = new Schema<ChangeData>(
+ 1, false, ImmutableList.<FieldDef<ChangeData, ?>> of(
+ ChangeField.STATUS));
+
+ private static Schema<ChangeData> V2 = new Schema<ChangeData>(
+ 2, false, ImmutableList.of(
+ ChangeField.STATUS,
+ ChangeField.FILE));
+
+ private static class DummyIndex implements ChangeIndex {
+ private final Schema<ChangeData> schema;
+
+ private DummyIndex(Schema<ChangeData> schema) {
+ this.schema = schema;
+ }
+
+ @Override
+ public ListenableFuture<Void> insert(ChangeData cd) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ListenableFuture<Void> replace(ChangeData cd) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ListenableFuture<Void> delete(ChangeData cd) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void deleteAll() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
+ throws QueryParseException {
+ return new Source(p);
+ }
+
+ @Override
+ public Schema<ChangeData> getSchema() {
+ return schema;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public void markReady(boolean ready) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private static class Source implements ChangeDataSource {
+ private final Predicate<ChangeData> p;
+
+ Source(Predicate<ChangeData> p) {
+ this.p = p;
+ }
+
+ @Override
+ public int getCardinality() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasChange() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ResultSet<ChangeData> read() throws OrmException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ return p.toString();
+ }
+ }
+
+ public class QueryBuilder extends ChangeQueryBuilder {
+ QueryBuilder() {
+ super(
+ new QueryBuilder.Definition<ChangeData, QueryBuilder>(
+ QueryBuilder.class),
+ new ChangeQueryBuilder.Arguments(null, null, null, null, null, null,
+ null, null, null, null, null, indexes),
+ null);
+ }
+
+ @Operator
+ public Predicate<ChangeData> foo(String value) {
+ return predicate("foo", value);
+ }
+
+ @Operator
+ public Predicate<ChangeData> bar(String value) {
+ return predicate("bar", value);
+ }
+
+ private Predicate<ChangeData> predicate(String name, String value) {
+ return new OperatorPredicate<ChangeData>(name, value) {
+ @Override
+ public boolean match(ChangeData object) throws OrmException {
+ return false;
+ }
+
+ @Override
+ public int getCost() {
+ return 0;
+ }
+ };
+ }
+ }
+
+ private DummyIndex index;
+ private IndexCollection indexes;
+ private ChangeQueryBuilder queryBuilder;
+ private IndexRewriteImpl rewrite;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ index = new DummyIndex(V2);
+ indexes = new IndexCollection();
+ indexes.setSearchIndex(index);
+ queryBuilder = new QueryBuilder();
+ rewrite = new IndexRewriteImpl(
+ indexes,
+ null,
+ new IndexRewriteImpl.BasicRewritesImpl(null),
+ new SqlRewriterImpl(null));
+ }
+
+ public void testIndexPredicate() throws Exception {
+ Predicate<ChangeData> in = parse("file:a");
+ assertEquals(query(in), rewrite(in));
+ }
+
+ public void testNonIndexPredicate() throws Exception {
+ Predicate<ChangeData> in = parse("foo:a");
+ assertSame(in, rewrite(in));
+ }
+
+ public void testIndexPredicates() throws Exception {
+ Predicate<ChangeData> in = parse("file:a file:b");
+ assertEquals(query(in), rewrite(in));
+ }
+
+ public void testNonIndexPredicates() throws Exception {
+ Predicate<ChangeData> in = parse("foo:a OR foo:b");
+ assertEquals(in, rewrite(in));
+ }
+
+ public void testOneIndexPredicate() throws Exception {
+ Predicate<ChangeData> in = parse("foo:a file:b");
+ Predicate<ChangeData> out = rewrite(in);
+ assertSame(AndSource.class, out.getClass());
+ assertEquals(
+ ImmutableList.of(query(in.getChild(1)), in.getChild(0)),
+ out.getChildren());
+ }
+
+ public void testThreeLevelTreeWithAllIndexPredicates() throws Exception {
+ Predicate<ChangeData> in =
+ parse("-status:abandoned (status:open OR status:merged)");
+ assertEquals(
+ query(parse("status:new OR status:submitted OR status:draft OR status:merged")),
+ rewrite.rewrite(in));
+ }
+
+ public void testThreeLevelTreeWithSomeIndexPredicates() throws Exception {
+ Predicate<ChangeData> in = parse("-foo:a (file:b OR file:c)");
+ Predicate<ChangeData> out = rewrite(in);
+ assertEquals(AndSource.class, out.getClass());
+ assertEquals(
+ ImmutableList.of(query(in.getChild(1)), in.getChild(0)),
+ out.getChildren());
+ }
+
+ public void testMultipleIndexPredicates() throws Exception {
+ Predicate<ChangeData> in =
+ parse("file:a OR foo:b OR file:c OR foo:d");
+ Predicate<ChangeData> out = rewrite(in);
+ assertSame(OrSource.class, out.getClass());
+ assertEquals(ImmutableList.of(
+ query(Predicate.or(in.getChild(0), in.getChild(2))),
+ in.getChild(1), in.getChild(3)),
+ out.getChildren());
+ }
+
+ public void testIndexAndNonIndexPredicates() throws Exception {
+ Predicate<ChangeData> in = parse("status:new bar:p file:a");
+ Predicate<ChangeData> out = rewrite(in);
+ assertSame(AndSource.class, out.getClass());
+ assertEquals(ImmutableList.of(
+ query(Predicate.and(in.getChild(0), in.getChild(2))),
+ in.getChild(1)),
+ out.getChildren());
+ }
+
+ public void testDuplicateCompoundNonIndexOnlyPredicates() throws Exception {
+ Predicate<ChangeData> in =
+ parse("(status:new OR status:draft) bar:p file:a");
+ Predicate<ChangeData> out = rewrite(in);
+ assertSame(AndSource.class, out.getClass());
+ assertEquals(ImmutableList.of(
+ query(Predicate.and(in.getChild(0), in.getChild(2))),
+ in.getChild(1)),
+ out.getChildren());
+ }
+
+ public void testDuplicateCompoundIndexOnlyPredicates() throws Exception {
+ Predicate<ChangeData> in =
+ parse("(status:new OR file:a) bar:p file:b");
+ Predicate<ChangeData> out = rewrite(in);
+ assertSame(AndSource.class, out.getClass());
+ assertEquals(ImmutableList.of(
+ query(Predicate.and(in.getChild(0), in.getChild(2))),
+ in.getChild(1)),
+ out.getChildren());
+ }
+
+ public void testLimit() throws Exception {
+ Predicate<ChangeData> in = parse("file:a limit:3");
+ Predicate<ChangeData> out = rewrite(in);
+ assertSame(AndSource.class, out.getClass());
+ assertEquals(ImmutableList.of(
+ query(in.getChild(0), 4),
+ in.getChild(1)),
+ out.getChildren());
+ }
+
+ public void testGetPossibleStatus() throws Exception {
+ assertEquals(EnumSet.allOf(Change.Status.class), status("file:a"));
+ assertEquals(EnumSet.of(NEW), status("is:new"));
+ assertEquals(EnumSet.of(SUBMITTED, DRAFT, MERGED, ABANDONED),
+ status("-is:new"));
+ assertEquals(EnumSet.of(NEW, MERGED), status("is:new OR is:merged"));
+
+ EnumSet<Change.Status> none = EnumSet.noneOf(Change.Status.class);
+ assertEquals(none, status("is:new is:merged"));
+ assertEquals(none, status("(is:new is:draft) (is:merged is:submitted)"));
+ assertEquals(none, status("(is:new is:draft) (is:merged is:submitted)"));
+
+ assertEquals(EnumSet.of(MERGED, SUBMITTED),
+ status("(is:new is:draft) OR (is:merged OR is:submitted)"));
+ }
+
+ public void testUnsupportedIndexOperator() throws Exception {
+ Predicate<ChangeData> in = parse("status:merged file:a");
+ assertEquals(query(in), rewrite(in));
+
+ indexes.setSearchIndex(new DummyIndex(V1));
+ Predicate<ChangeData> out = rewrite(in);
+ assertTrue(out instanceof AndPredicate);
+ assertEquals(ImmutableList.of(
+ query(in.getChild(0)),
+ in.getChild(1)),
+ out.getChildren());
+ }
+
+ public void testNoChangeIndexUsesSqlRewrites() throws Exception {
+ Predicate<ChangeData> in = parse("status:open project:p ref:b");
+ Predicate<ChangeData> out;
+
+ out = rewrite(in);
+ assertTrue(out instanceof AndPredicate || out instanceof IndexedChangeQuery);
+
+ indexes.setSearchIndex(null);
+ out = rewrite(in);
+ assertTrue(out instanceof RewritePredicate);
+ }
+
+ private Predicate<ChangeData> parse(String query) throws QueryParseException {
+ return queryBuilder.parse(query);
+ }
+
+ private Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
+ throws QueryParseException {
+ return rewrite.rewrite(in);
+ }
+
+ private IndexedChangeQuery query(Predicate<ChangeData> p)
+ throws QueryParseException {
+ return query(p, IndexRewriteImpl.MAX_LIMIT);
+ }
+
+ private IndexedChangeQuery query(Predicate<ChangeData> p, int limit)
+ throws QueryParseException {
+ return new IndexedChangeQuery(index, p, limit);
+ }
+
+ private Set<Change.Status> status(String query) throws QueryParseException {
+ return IndexRewriteImpl.getPossibleStatus(parse(query));
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index f1bb7de..968d661 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -420,10 +420,19 @@
}
@Override
+ public ProjectState checkedGet(Project.NameKey projectName) {
+ return get(projectName);
+ }
+
+ @Override
public void evict(Project p) {
}
@Override
+ public void evict(Project.NameKey p) {
+ }
+
+ @Override
public void remove(Project p) {
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java
index ce7b25c..1500272 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java
@@ -76,7 +76,7 @@
private static ChangeData change(String... files) {
Arrays.sort(files);
ChangeData cd = new ChangeData(new Change.Id(1));
- cd.setCurrentFilePaths(files);
+ cd.setCurrentFilePaths(Arrays.asList(files));
return cd;
}
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
index b517de7..47ff6b0 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
@@ -52,6 +52,8 @@
import static org.junit.Assert.fail;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
@@ -64,10 +66,13 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
+import java.util.List;
+import java.util.Map;
public abstract class HookTestCase extends LocalDiskRepositoryTestCase {
protected Repository repository;
- private File hooksh;
+ private final Map<String, File> hooks = Maps.newTreeMap();
+ private final List<File> cleanup = Lists.newArrayList();
@Override
@Before
@@ -79,15 +84,21 @@
@Override
@After
public void tearDown() throws Exception {
- if (hooksh != null) {
- if (!hooksh.delete()) {
- hooksh.deleteOnExit();
+ super.tearDown();
+ for (File p : cleanup) {
+ if (!p.delete()) {
+ p.deleteOnExit();
}
- hooksh = null;
}
+ cleanup.clear();
}
protected File getHook(final String name) throws IOException {
+ File hook = hooks.get(name);
+ if (hook != null) {
+ return hook;
+ }
+
final String scproot = "com/google/gerrit/server/tools/root";
final String path = scproot + "/hooks/" + name;
URL url = cl().getResource(path);
@@ -95,17 +106,22 @@
fail("Cannot locate " + path + " in CLASSPATH");
}
- File hook;
if ("file".equals(url.getProtocol())) {
hook = new File(url.getPath());
if (!hook.isFile()) {
fail("Cannot locate " + path + " in CLASSPATH");
}
+ long time = hook.lastModified();
+ hook.setExecutable(true);
+ hook.setLastModified(time);
+ hooks.put(name, hook);
+ return hook;
} else if ("jar".equals(url.getProtocol())) {
- hooksh = File.createTempFile("hook_", ".sh");
InputStream in = url.openStream();
try {
- FileOutputStream out = new FileOutputStream(hooksh);
+ hook = File.createTempFile("hook_", ".sh");
+ cleanup.add(hook);
+ FileOutputStream out = new FileOutputStream(hook);
try {
ByteStreams.copy(in, out);
} finally {
@@ -114,21 +130,13 @@
} finally {
in.close();
}
- hook = hooksh;
+ hook.setExecutable(true);
+ hooks.put(name, hook);
+ return hook;
} else {
fail("Cannot invoke " + url);
- hook = null;
+ return null;
}
-
- // The hook was copied out of our source control system into the
- // target area by Java tools. Its not executable in the source
- // are, nor did the copying Java program make it executable in the
- // destination area. So we must force it to be executable.
- //
- final long time = hook.lastModified();
- hook.setExecutable(true);
- hook.setLastModified(time);
- return hook;
}
private ClassLoader cl() {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
index 7294d4c..299a245 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
@@ -41,7 +41,7 @@
import java.util.TreeSet;
public class SubmoduleSectionParserTest extends LocalDiskRepositoryTestCase {
- private final static String THIS_SERVER = "localhost";
+ private static final String THIS_SERVER = "localhost";
private GitRepositoryManager repoManager;
private BlobBasedConfig bbc;
@@ -245,7 +245,7 @@
assertEquals(expectedSubscriptions, returnedSubscriptions);
}
- private final static class SubmoduleSection {
+ private static final class SubmoduleSection {
private final String url;
private final String path;
private final String branch;
diff --git a/gerrit-solr/BUCK b/gerrit-solr/BUCK
new file mode 100644
index 0000000..c072830
--- /dev/null
+++ b/gerrit-solr/BUCK
@@ -0,0 +1,19 @@
+java_library(
+ name = 'solr',
+ srcs = glob(['src/main/java/**/*.java']),
+ deps = [
+ '//gerrit-antlr:query_exception',
+ '//gerrit-extension-api:api',
+ '//gerrit-lucene:query_builder',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//lib:guava',
+ '//lib:gwtorm',
+ '//lib/guice:guice',
+ '//lib/jgit:jgit',
+ '//lib/log:api',
+ '//lib/lucene:core',
+ '//lib/solr:solrj',
+ ],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java
new file mode 100644
index 0000000..7e32bac
--- /dev/null
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/IndexVersionCheck.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.solr;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+
+class IndexVersionCheck implements LifecycleListener {
+ public static final Map<String, Integer> SCHEMA_VERSIONS = ImmutableMap.of(
+ SolrChangeIndex.CHANGES_OPEN, ChangeSchemas.getLatest().getVersion(),
+ SolrChangeIndex.CHANGES_CLOSED, ChangeSchemas.getLatest().getVersion());
+
+ public static File solrIndexConfig(SitePaths sitePaths) {
+ return new File(sitePaths.index_dir, "gerrit_index.config");
+ }
+
+ private final SitePaths sitePaths;
+
+ @Inject
+ IndexVersionCheck(SitePaths sitePaths) {
+ this.sitePaths = sitePaths;
+ }
+
+ @Override
+ public void start() {
+ // TODO Query schema version from a special meta-document
+ File file = solrIndexConfig(sitePaths);
+ try {
+ FileBasedConfig cfg = new FileBasedConfig(file, FS.detect());
+ cfg.load();
+ for (Map.Entry<String, Integer> e : SCHEMA_VERSIONS.entrySet()) {
+ int schemaVersion = cfg.getInt("index", e.getKey(), "schemaVersion", 0);
+ if (schemaVersion != e.getValue()) {
+ throw new ProvisionException(String.format(
+ "wrong index schema version for \"%s\": expected %d, found %d%s",
+ e.getKey(), e.getValue(), schemaVersion, upgrade()));
+ }
+ }
+ } catch (IOException e) {
+ throw new ProvisionException("unable to read " + file);
+ } catch (ConfigInvalidException e) {
+ throw new ProvisionException("invalid config file " + file);
+ }
+ }
+
+ @Override
+ public void stop() {
+ // Do nothing.
+ }
+
+ private final String upgrade() {
+ return "\nRun reindex to rebuild the index:\n"
+ + "$ java -jar gerrit.war reindex -d "
+ + sitePaths.site_path.getAbsolutePath();
+ }
+}
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
new file mode 100644
index 0000000..33150e8
--- /dev/null
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
@@ -0,0 +1,340 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.solr;
+
+import static com.google.gerrit.server.index.IndexRewriteImpl.CLOSED_STATUSES;
+import static com.google.gerrit.server.index.IndexRewriteImpl.OPEN_STATUSES;
+import static com.google.gerrit.solr.IndexVersionCheck.SCHEMA_VERSIONS;
+import static com.google.gerrit.solr.IndexVersionCheck.solrIndexConfig;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lucene.QueryBuilder;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.FieldDef;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
+import com.google.gerrit.server.index.FieldType;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexRewriteImpl;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+import org.apache.lucene.search.Query;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.SolrServer;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudSolrServer;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.SolrInputDocument;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Secondary index implementation using a remote Solr instance. */
+class SolrChangeIndex implements ChangeIndex, LifecycleListener {
+ public static final String CHANGES_OPEN = "changes_open";
+ public static final String CHANGES_CLOSED = "changes_closed";
+ private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
+
+ private final FillArgs fillArgs;
+ private final SitePaths sitePaths;
+ private final IndexCollection indexes;
+ private final CloudSolrServer openIndex;
+ private final CloudSolrServer closedIndex;
+ private final Schema<ChangeData> schema;
+
+ SolrChangeIndex(
+ @GerritServerConfig Config cfg,
+ FillArgs fillArgs,
+ SitePaths sitePaths,
+ IndexCollection indexes,
+ Schema<ChangeData> schema,
+ String base) throws IOException {
+ this.fillArgs = fillArgs;
+ this.sitePaths = sitePaths;
+ this.indexes = indexes;
+ this.schema = schema;
+
+ String url = cfg.getString("index", "solr", "url");
+ if (Strings.isNullOrEmpty(url)) {
+ throw new IllegalStateException("index.solr.url must be supplied");
+ }
+
+ base = Strings.nullToEmpty(base);
+ openIndex = new CloudSolrServer(url);
+ openIndex.setDefaultCollection(base + CHANGES_OPEN);
+
+ closedIndex = new CloudSolrServer(url);
+ closedIndex.setDefaultCollection(base + CHANGES_CLOSED);
+ }
+
+ @Override
+ public void start() {
+ indexes.setSearchIndex(this);
+ indexes.addWriteIndex(this);
+ }
+
+ @Override
+ public void stop() {
+ openIndex.shutdown();
+ closedIndex.shutdown();
+ }
+
+ @Override
+ public Schema<ChangeData> getSchema() {
+ return schema;
+ }
+
+ @Override
+ public void close() {
+ stop();
+ }
+
+ @Override
+ public ListenableFuture<Void> insert(ChangeData cd) throws IOException {
+ String id = cd.getId().toString();
+ SolrInputDocument doc = toDocument(cd);
+ try {
+ if (cd.getChange().getStatus().isOpen()) {
+ closedIndex.deleteById(id);
+ openIndex.add(doc);
+ } else {
+ openIndex.deleteById(id);
+ closedIndex.add(doc);
+ }
+ } catch (SolrServerException e) {
+ throw new IOException(e);
+ }
+ commit(openIndex);
+ commit(closedIndex);
+ return Futures.immediateFuture(null);
+ }
+
+ @Override
+ public ListenableFuture<Void> replace(ChangeData cd) throws IOException {
+ String id = cd.getId().toString();
+ SolrInputDocument doc = toDocument(cd);
+ try {
+ if (cd.getChange().getStatus().isOpen()) {
+ closedIndex.deleteById(id);
+ openIndex.add(doc);
+ } else {
+ openIndex.deleteById(id);
+ closedIndex.add(doc);
+ }
+ } catch (SolrServerException e) {
+ throw new IOException(e);
+ }
+ commit(openIndex);
+ commit(closedIndex);
+ return Futures.immediateFuture(null);
+ }
+
+ @Override
+ public ListenableFuture<Void> delete(ChangeData cd) throws IOException {
+ String id = cd.getId().toString();
+ try {
+ if (cd.getChange().getStatus().isOpen()) {
+ openIndex.deleteById(id);
+ commit(openIndex);
+ } else {
+ closedIndex.deleteById(id);
+ commit(closedIndex);
+ }
+ return Futures.immediateFuture(null);
+ } catch (SolrServerException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public void deleteAll() throws IOException {
+ try {
+ openIndex.deleteByQuery("*:*");
+ closedIndex.deleteByQuery("*:*");
+ } catch (SolrServerException e) {
+ throw new IOException(e);
+ }
+ commit(openIndex);
+ commit(closedIndex);
+ }
+
+ @Override
+ public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
+ throws QueryParseException {
+ Set<Change.Status> statuses = IndexRewriteImpl.getPossibleStatus(p);
+ List<SolrServer> indexes = Lists.newArrayListWithCapacity(2);
+ if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
+ indexes.add(openIndex);
+ }
+ if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
+ indexes.add(closedIndex);
+ }
+ return new QuerySource(indexes, QueryBuilder.toQuery(p), limit);
+ }
+
+ private void commit(SolrServer server) throws IOException {
+ try {
+ server.commit();
+ } catch (SolrServerException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private class QuerySource implements ChangeDataSource {
+ private final List<SolrServer> indexes;
+ private final SolrQuery query;
+
+ public QuerySource(List<SolrServer> indexes, Query q, int limit) {
+ this.indexes = indexes;
+
+ query = new SolrQuery(q.toString());
+ query.setParam("shards.tolerant", true);
+ query.setParam("rows", Integer.toString(limit));
+ query.setFields(ID_FIELD);
+ query.setSort(
+ ChangeField.UPDATED.getName(),
+ SolrQuery.ORDER.desc);
+ }
+
+ @Override
+ public int getCardinality() {
+ return 10; // TODO: estimate from solr?
+ }
+
+ @Override
+ public boolean hasChange() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return query.getQuery();
+ }
+
+ @Override
+ public ResultSet<ChangeData> read() throws OrmException {
+ try {
+ // TODO Sort documents during merge to select only top N.
+ SolrDocumentList docs = new SolrDocumentList();
+ for (SolrServer index : indexes) {
+ docs.addAll(index.query(query).getResults());
+ }
+
+ List<ChangeData> result = Lists.newArrayListWithCapacity(docs.size());
+ for (SolrDocument doc : docs) {
+ Integer v = (Integer) doc.getFieldValue(ID_FIELD);
+ result.add(new ChangeData(new Change.Id(v.intValue())));
+ }
+
+ final List<ChangeData> r = Collections.unmodifiableList(result);
+ return new ResultSet<ChangeData>() {
+ @Override
+ public Iterator<ChangeData> iterator() {
+ return r.iterator();
+ }
+
+ @Override
+ public List<ChangeData> toList() {
+ return r;
+ }
+
+ @Override
+ public void close() {
+ // Do nothing.
+ }
+ };
+ } catch (SolrServerException e) {
+ throw new OrmException(e);
+ }
+ }
+ }
+
+ private SolrInputDocument toDocument(ChangeData cd) throws IOException {
+ try {
+ SolrInputDocument result = new SolrInputDocument();
+ for (FieldDef<ChangeData, ?> f : schema.getFields().values()) {
+ if (f.isRepeatable()) {
+ add(result, f, (Iterable<?>) f.get(cd, fillArgs));
+ } else {
+ add(result, f, Collections.singleton(f.get(cd, fillArgs)));
+ }
+ }
+ return result;
+ } catch (OrmException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private void add(SolrInputDocument doc, FieldDef<ChangeData, ?> f,
+ Iterable<?> values) throws OrmException {
+ if (f.getType() == FieldType.INTEGER) {
+ for (Object value : values) {
+ doc.addField(f.getName(), (Integer) value);
+ }
+ } else if (f.getType() == FieldType.LONG) {
+ for (Object value : values) {
+ doc.addField(f.getName(), (Long) value);
+ }
+ } else if (f.getType() == FieldType.TIMESTAMP) {
+ for (Object v : values) {
+ doc.addField(f.getName(), QueryBuilder.toIndexTime((Timestamp) v));
+ }
+ } else if (f.getType() == FieldType.EXACT
+ || f.getType() == FieldType.PREFIX
+ || f.getType() == FieldType.FULL_TEXT) {
+ for (Object value : values) {
+ doc.addField(f.getName(), (String) value);
+ }
+ } else {
+ throw QueryBuilder.badFieldType(f.getType());
+ }
+ }
+
+ @Override
+ public void markReady(boolean ready) throws IOException {
+ // TODO Move the schema version information to a special meta-document
+ FileBasedConfig cfg = new FileBasedConfig(
+ solrIndexConfig(sitePaths),
+ FS.detect());
+ for (Map.Entry<String, Integer> e : SCHEMA_VERSIONS.entrySet()) {
+ cfg.setInt("index", e.getKey(), "schemaVersion",
+ ready ? e.getValue() : -1);
+ }
+ cfg.save();
+ }
+}
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
new file mode 100644
index 0000000..8c614f7
--- /dev/null
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrIndexModule.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.solr;
+
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.ChangeIndex;
+import com.google.gerrit.server.index.ChangeSchemas;
+import com.google.gerrit.server.index.FieldDef.FillArgs;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+
+public class SolrIndexModule extends LifecycleModule {
+ private final boolean checkVersion;
+ private final int threads;
+ private final String base;
+
+ public SolrIndexModule() {
+ this(true, 0, null);
+ }
+
+ public SolrIndexModule(boolean checkVersion, int threads, String base) {
+ this.checkVersion = checkVersion;
+ this.threads = threads;
+ this.base = base;
+ }
+
+ @Override
+ protected void configure() {
+ install(new IndexModule(threads));
+ bind(ChangeIndex.class).to(SolrChangeIndex.class);
+ listener().to(SolrChangeIndex.class);
+ if (checkVersion) {
+ listener().to(IndexVersionCheck.class);
+ }
+ }
+
+ @Provides
+ @Singleton
+ public SolrChangeIndex getChangeIndex(@GerritServerConfig Config cfg,
+ SitePaths sitePaths,
+ IndexCollection indexes,
+ FillArgs fillArgs) throws IOException {
+ return new SolrChangeIndex(cfg, fillArgs, sitePaths, indexes,
+ ChangeSchemas.getLatest(), base);
+ }
+}
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK
new file mode 100644
index 0000000..7a8610d
--- /dev/null
+++ b/gerrit-sshd/BUCK
@@ -0,0 +1,40 @@
+SRCS = glob(['src/main/java/**/*.java'])
+
+java_library2(
+ name = 'sshd',
+ srcs = SRCS,
+ deps = [
+ '//gerrit-extension-api:api',
+ '//gerrit-cache-h2:cache-h2',
+ '//gerrit-common:server',
+ '//gerrit-patch-jgit:server',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//gerrit-util-cli:cli',
+ '//lib:args4j',
+ '//lib:gson',
+ '//lib:guava',
+ '//lib:gwtorm',
+ '//lib:jsch',
+ '//lib:jsr305',
+ '//lib/commons:codec',
+ '//lib/guice:guice',
+ '//lib/guice:guice-assistedinject',
+ '//lib/guice:guice-servlet', # SSH should not depend on servlet
+ '//lib/log:api',
+ '//lib/log:log4j',
+ '//lib/mina:core',
+ '//lib/mina:sshd',
+ '//lib/jgit:jgit',
+ ],
+ compile_deps = [
+ '//lib/bouncycastle:bcprov',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+java_sources(
+ name = 'sshd-src',
+ srcs = SRCS,
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
deleted file mode 100644
index ab9bd43..0000000
--- a/gerrit-sshd/pom.xml
+++ /dev/null
@@ -1,79 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-sshd</artifactId>
- <name>Gerrit Code Review - SSHd</name>
-
- <description>
- Java SSH daemon with Gerrit commands and Git support
- </description>
-
- <dependencies>
- <dependency>
- <groupId>log4j</groupId>
- <artifactId>log4j</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit.junit</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.apache.mina</groupId>
- <artifactId>mina-core</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.apache.sshd</groupId>
- <artifactId>sshd-core</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-util-cli</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-server</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-cache-h2</artifactId>
- <version>${project.version}</version>
- </dependency>
- </dependencies>
-</project>
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index 98b740c..21937e4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -15,6 +15,7 @@
package com.google.gerrit.sshd;
import com.google.common.util.concurrent.Atomics;
+import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gerrit.server.CurrentUser;
@@ -53,6 +54,8 @@
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
+import javax.annotation.Nullable;
+
public abstract class BaseCommand implements Command {
private static final Logger log = LoggerFactory.getLogger(BaseCommand.class);
public static final String ENC = "UTF-8";
@@ -90,6 +93,11 @@
@Inject
private Provider<SshScope.Context> contextProvider;
+ /** Commands declared by a plugin can be scoped by the plugin name. */
+ @Inject(optional = true)
+ @PluginName
+ private String pluginName;
+
/** The task, as scheduled on a worker thread. */
private final AtomicReference<Future<?>> task;
@@ -119,6 +127,11 @@
this.exit = callback;
}
+ @Nullable
+ String getPluginName() {
+ return pluginName;
+ }
+
String getName() {
return commandName;
}
@@ -326,7 +339,7 @@
} else {
final StringBuilder m = new StringBuilder();
m.append("Internal server error");
- if (userProvider.get() instanceof IdentifiedUser) {
+ if (userProvider.get().isIdentifiedUser()) {
final IdentifiedUser u = (IdentifiedUser) userProvider.get();
m.append(" (user ");
m.append(u.getAccount().getUserName());
@@ -390,7 +403,7 @@
StringBuilder m = new StringBuilder();
m.append(context.getCommandLine());
- if (userProvider.get() instanceof IdentifiedUser) {
+ if (userProvider.get().isIdentifiedUser()) {
IdentifiedUser u = (IdentifiedUser) userProvider.get();
m.append(" (" + u.getAccount().getUserName() + ")");
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 910f5f3..a2e6542 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -16,6 +16,7 @@
import com.google.common.util.concurrent.Atomics;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.WorkQueue;
@@ -23,6 +24,7 @@
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import com.google.inject.Singleton;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.CommandFactory;
@@ -39,7 +41,7 @@
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
@@ -49,7 +51,9 @@
/**
* Creates a CommandFactory using commands registered by {@link CommandModule}.
*/
-class CommandFactoryProvider implements Provider<CommandFactory> {
+@Singleton
+class CommandFactoryProvider implements Provider<CommandFactory>,
+ LifecycleListener {
private static final Logger logger = LoggerFactory
.getLogger(CommandFactoryProvider.class);
@@ -57,7 +61,7 @@
private final SshLog log;
private final SshScope sshScope;
private final ScheduledExecutorService startExecutor;
- private final Executor destroyExecutor;
+ private final ExecutorService destroyExecutor;
private final SchemaFactory<ReviewDb> schemaFactory;
@Inject
@@ -80,6 +84,15 @@
}
@Override
+ public void start() {
+ }
+
+ @Override
+ public void stop() {
+ destroyExecutor.shutdownNow();
+ }
+
+ @Override
public CommandFactory get() {
return new CommandFactory() {
public Command createCommand(final String requestCommand) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
index cfcee6a..42c0fd7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
@@ -27,5 +27,9 @@
@Retention(RUNTIME)
public @interface CommandMetaData {
String name();
+ String description() default "";
+
+ /** @deprecated use description intead. */
+ @Deprecated
String descr() default "";
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
index e7e8a44..c64f9d8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
@@ -14,6 +14,8 @@
package com.google.gerrit.sshd;
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
import com.google.inject.AbstractModule;
import com.google.inject.binder.LinkedBindingBuilder;
@@ -74,7 +76,7 @@
if (meta == null) {
throw new IllegalStateException("no CommandMetaData annotation found");
}
- bind(Commands.key(parent, meta.name(), meta.descr())).to(clazz);
+ bind(Commands.key(parent, meta.name(), description(meta))).to(clazz);
}
/**
@@ -93,7 +95,14 @@
if (meta == null) {
throw new IllegalStateException("no CommandMetaData annotation found");
}
- bind(Commands.key(parent, name, meta.descr())).to(clazz);
+ bind(Commands.key(parent, name, description(meta))).to(clazz);
+ }
+
+ @SuppressWarnings("deprecation")
+ private static String description(CommandMetaData meta) {
+ return Objects.firstNonNull(
+ Strings.emptyToNull(meta.description()),
+ meta.descr());
}
/**
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
index 929a895..1a5e62cc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
@@ -16,7 +16,6 @@
import com.google.inject.Key;
-import org.apache.commons.lang.StringUtils;
import org.apache.sshd.server.Command;
import java.lang.annotation.Annotation;
@@ -124,7 +123,7 @@
NestedCommandNameImpl(final CommandName parent, final String name) {
this.parent = parent;
this.name = name;
- this.descr = StringUtils.EMPTY;
+ this.descr = "";
}
NestedCommandNameImpl(final CommandName parent, final String name,
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index 455b732..fa5ab53 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -17,9 +17,9 @@
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Atomics;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.CapabilityUtils;
import com.google.gerrit.server.args4j.SubcommandHandler;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -113,17 +113,18 @@
}
}
- private void checkRequiresCapability(Command cmd) throws UnloggedFailure {
- RequiresCapability rc = cmd.getClass().getAnnotation(RequiresCapability.class);
- if (rc != null) {
- CurrentUser user = currentUser.get();
- CapabilityControl ctl = user.getCapabilities();
- if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
- String msg = String.format(
- "fatal: %s does not have \"%s\" capability.",
- user.getUserName(), rc.value());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
+ private void checkRequiresCapability(Command cmd)
+ throws UnloggedFailure {
+ String pluginName = null;
+ if (cmd instanceof BaseCommand) {
+ pluginName = ((BaseCommand) cmd).getPluginName();
+ }
+ try {
+ CapabilityUtils.checkRequiresCapability(currentUser,
+ pluginName, cmd.getClass());
+ } catch (AuthException e) {
+ throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN,
+ e.getMessage());
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
index 013f070..334f155 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
@@ -26,7 +26,7 @@
private CommandName command;
@Inject
- void setPluginName(@PluginName String name, final String descr) {
+ void setPluginName(@PluginName String name) {
this.command = Commands.named(name);
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index 336490c..8c17407 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -233,7 +233,7 @@
String userName = "-", accountId = "-";
- if (user instanceof IdentifiedUser) {
+ if (user != null && user.isIdentifiedUser()) {
IdentifiedUser u = (IdentifiedUser) user;
userName = u.getAccount().getUserName();
accountId = "a/" + u.getAccountId().toString();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index 2e42c23..48c565d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -14,12 +14,11 @@
package com.google.gerrit.sshd;
-import static com.google.inject.Scopes.SINGLETON;
import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
+import static com.google.inject.Scopes.SINGLETON;
import com.google.common.collect.Maps;
import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.CmdLineParserModule;
import com.google.gerrit.server.PeerDaemonUser;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.config.FactoryModule;
@@ -68,7 +67,6 @@
configureRequestScope();
install(new AsyncReceiveCommits.Module());
- install(new CmdLineParserModule());
configureAliases();
bind(SshLog.class);
@@ -107,6 +105,7 @@
listener().toInstance(registerInParentInjectors());
listener().to(SshLog.class);
listener().to(SshDaemon.class);
+ listener().to(CommandFactoryProvider.class);
}
});
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
index 0ae40a5..9b19e02 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
@@ -81,7 +81,7 @@
@Override
public CurrentUser getCurrentUser() {
final CurrentUser user = session.getCurrentUser();
- if (user instanceof IdentifiedUser) {
+ if (user != null && user.isIdentifiedUser()) {
IdentifiedUser identifiedUser = userFactory.create(((IdentifiedUser) user).getAccountId());
identifiedUser.setAccessPath(user.getAccessPath());
return identifiedUser;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
index 2cc16b5..04bf603 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
@@ -76,6 +76,7 @@
void authenticationSuccess(String user, CurrentUser id) {
username = user;
identity = id;
+ identity.setAccessPath(AccessPath.SSH_COMMAND);
authError = null;
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
index 6a4d995..fc1303c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
@@ -79,7 +79,7 @@
final StringBuilder strBuf = new StringBuilder();
final BufferedReader br = new BufferedReader(new StringReader(keyStr));
String line = br.readLine(); // BEGIN SSH2 line...
- if (!line.equals("---- BEGIN SSH2 PUBLIC KEY ----")) {
+ if (line == null || !line.equals("---- BEGIN SSH2 PUBLIC KEY ----")) {
return keyStr;
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index ccf23e1..298a099 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -19,6 +19,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PeerDaemonUser;
+import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.sshd.SshScope.Context;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -45,6 +46,7 @@
private final SshScope sshScope;
private final DispatchCommandProvider dispatcher;
+ private boolean enableRunAs;
private Provider<CurrentUser> caller;
private Provider<SshSession> session;
private IdentifiedUser.GenericFactory userFactory;
@@ -66,36 +68,34 @@
@CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
final Provider<CurrentUser> caller, final Provider<SshSession> session,
final IdentifiedUser.GenericFactory userFactory,
- final SshScope.Context callingContext) {
+ final SshScope.Context callingContext,
+ AuthConfig config) {
this.sshScope = sshScope;
this.dispatcher = dispatcher;
this.caller = caller;
this.session = session;
this.userFactory = userFactory;
this.callingContext = callingContext;
+ this.enableRunAs = config.isRunAsEnabled();
atomicCmd = Atomics.newReference();
}
@Override
public void start(Environment env) throws IOException {
try {
- if (caller.get() instanceof PeerDaemonUser) {
- parseCommandLine();
+ checkCanRunAs();
+ parseCommandLine();
- final Context ctx = callingContext.subContext(newSession(), join(args));
- final Context old = sshScope.set(ctx);
- try {
- final BaseCommand cmd = dispatcher.get();
- cmd.setArguments(args.toArray(new String[args.size()]));
- provideStateTo(cmd);
- atomicCmd.set(cmd);
- cmd.start(env);
- } finally {
- sshScope.set(old);
- }
-
- } else {
- throw new UnloggedFailure(1, "fatal: Not a peer daemon");
+ final Context ctx = callingContext.subContext(newSession(), join(args));
+ final Context old = sshScope.set(ctx);
+ try {
+ final BaseCommand cmd = dispatcher.get();
+ cmd.setArguments(args.toArray(new String[args.size()]));
+ provideStateTo(cmd);
+ atomicCmd.set(cmd);
+ cmd.start(env);
+ } finally {
+ sshScope.set(old);
}
} catch (UnloggedFailure e) {
String msg = e.getMessage();
@@ -108,6 +108,17 @@
}
}
+ private void checkCanRunAs() throws UnloggedFailure {
+ if (caller.get() instanceof PeerDaemonUser) {
+ // OK.
+ } else if (!enableRunAs) {
+ throw new UnloggedFailure(1,
+ "fatal: suexec disabled by auth.enableRunAs = false");
+ } else if (!caller.get().getCapabilities().canRunAs()) {
+ throw new UnloggedFailure(1, "fatal: suexec not permitted");
+ }
+ }
+
private SshSession newSession() {
final SocketAddress peer;
if (peerAddress == null) {
@@ -115,8 +126,12 @@
} else {
peer = peerAddress;
}
+ CurrentUser self = caller.get();
+ if (self instanceof PeerDaemonUser) {
+ self = null;
+ }
return new SshSession(session.get(), peer,
- userFactory.create(peer, accountId));
+ userFactory.runAs(peer, accountId, self));
}
private static String join(List<String> args) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
index 15229dc..0ff3a01 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
@@ -28,7 +28,7 @@
/** Opens a query processor. */
@AdminHighPriorityCommand
@RequiresCapability(GlobalCapability.ACCESS_DATABASE)
-@CommandMetaData(name = "gsql", descr = "Administrative interface to active database")
+@CommandMetaData(name = "gsql", description = "Administrative interface to active database")
final class AdminQueryShell extends SshCommand {
@Inject
private QueryShell.Factory factory;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index faf2224..c68dc26 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -17,18 +17,23 @@
import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ListChildProjects;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
+import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -45,7 +50,7 @@
import java.util.Set;
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "set-project-parent", descr = "Change the project permissions are inherited from")
+@CommandMetaData(name = "set-project-parent", description = "Change the project permissions are inherited from")
final class AdminSetParent extends SshCommand {
private static final Logger log = LoggerFactory.getLogger(AdminSetParent.class);
@@ -73,6 +78,9 @@
@Inject
private AllProjectsName allProjectsName;
+ @Inject
+ private Provider<ListChildProjects> listChildProjects;
+
private Project.NameKey newParentKey = null;
@Override
@@ -108,17 +116,16 @@
}
}
- final List<Project> childProjects = new ArrayList<Project>();
+ final List<Project.NameKey> childProjects = Lists.newArrayList();
for (final ProjectControl pc : children) {
- childProjects.add(pc.getProject());
+ childProjects.add(pc.getProject().getNameKey());
}
if (oldParent != null) {
childProjects.addAll(getChildrenForReparenting(oldParent));
}
- for (final Project project : childProjects) {
- final String name = project.getName();
- final Project.NameKey nameKey = project.getNameKey();
+ for (final Project.NameKey nameKey : childProjects) {
+ final String name = nameKey.get();
if (allProjectsName.equals(nameKey)) {
// Don't allow the wild card project to have a parent.
@@ -159,7 +166,7 @@
err.append("error: " + msg + "\n");
}
- projectCache.evict(project);
+ projectCache.evict(nameKey);
}
if (err.length() > 0) {
@@ -175,8 +182,8 @@
* reparented. The returned list of child projects does not contain projects
* that were specified to be excluded from reparenting.
*/
- private List<Project> getChildrenForReparenting(final ProjectControl parent) {
- final List<Project> childProjects = new ArrayList<Project>();
+ private List<Project.NameKey> getChildrenForReparenting(final ProjectControl parent) {
+ final List<Project.NameKey> childProjects = Lists.newArrayList();
final List<Project.NameKey> excluded =
new ArrayList<Project.NameKey>(excludedChildren.size());
for (final ProjectControl excludedChild : excludedChildren) {
@@ -187,11 +194,12 @@
if (newParentKey != null) {
automaticallyExcluded.addAll(getAllParents(newParentKey));
}
- for (final Project child : getChildren(parent.getProject().getNameKey())) {
- final Project.NameKey childName = child.getNameKey();
+ for (final ProjectInfo child : listChildProjects.get().apply(
+ new ProjectResource(parent))) {
+ final Project.NameKey childName = new Project.NameKey(child.name);
if (!excluded.contains(childName)) {
if (!automaticallyExcluded.contains(childName)) {
- childProjects.add(child);
+ childProjects.add(childName);
} else {
stdout.println("Automatically excluded '" + childName + "' " +
"from reparenting because it is in the parent " +
@@ -213,20 +221,4 @@
}
}));
}
-
- private List<Project> getChildren(final Project.NameKey parentName) {
- final List<Project> childProjects = new ArrayList<Project>();
- for (final Project.NameKey projectName : projectCache.all()) {
- final ProjectState e = projectCache.get(projectName);
- if (e == null) {
- // If we can't get it from the cache, pretend it's not present.
- continue;
- }
-
- if (parentName.equals(e.getProject().getParent(allProjectsName))) {
- childProjects.add(e.getProject());
- }
- }
- return childProjects;
- }
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index 0268bc0..dc22a29 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -33,7 +33,7 @@
import java.util.ArrayList;
import java.util.List;
-@CommandMetaData(name = "ban-commit", descr = "Ban a commit from a project's repository")
+@CommandMetaData(name = "ban-commit", description = "Ban a commit from a project's repository")
public class BanCommitCommand extends SshCommand {
@Option(name = "--reason", aliases = {"-r"}, metaVar = "REASON", usage = "reason for banning the commit")
private String reason;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
index 500c84a..7f9e5db 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
@@ -28,10 +28,8 @@
protected SortedSet<String> cacheNames() {
SortedSet<String> names = Sets.newTreeSet();
- for (String plugin : cacheMap.plugins()) {
- for (String name : cacheMap.byPlugin(plugin).keySet()) {
- names.add(cacheNameOf(plugin, name));
- }
+ for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
+ names.add(cacheNameOf(e.getPluginName(), e.getExportName()));
}
return names;
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index c209249..aef1560 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -14,23 +14,16 @@
package com.google.gerrit.sshd.commands;
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountByEmailCache;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.server.account.CreateAccount;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -42,13 +35,11 @@
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
import java.util.List;
/** Create a new user account. **/
@RequiresCapability(GlobalCapability.CREATE_ACCOUNT)
-@CommandMetaData(name = "create-account", descr = "Create a new batch/role account")
+@CommandMetaData(name = "create-account", description = "Create a new batch/role account")
final class CreateAccountCommand extends SshCommand {
@Option(name = "--group", aliases = {"-g"}, metaVar = "GROUP", usage = "groups to add account to")
private List<AccountGroup.Id> groups = new ArrayList<AccountGroup.Id>();
@@ -69,94 +60,29 @@
private String username;
@Inject
- private IdentifiedUser currentUser;
-
- @Inject
- private ReviewDb db;
-
- @Inject
- private SshKeyCache sshKeyCache;
-
- @Inject
- private AccountCache accountCache;
-
- @Inject
- private AccountByEmailCache byEmailCache;
+ private CreateAccount.Factory createAccountFactory;
@Override
- protected void run() throws OrmException, IOException,
- InvalidSshKeyException, UnloggedFailure {
- if (!username.matches(Account.USER_NAME_PATTERN)) {
- throw die("Username '" + username + "'"
- + " must contain only letters, numbers, _, - or .");
- }
-
- final Account.Id id = new Account.Id(db.nextAccountId());
- final AccountSshKey key = readSshKey(id);
-
- AccountExternalId extUser =
- new AccountExternalId(id, new AccountExternalId.Key(
- AccountExternalId.SCHEME_USERNAME, username));
-
- if (httpPassword != null) {
- extUser.setPassword(httpPassword);
- }
-
- if (db.accountExternalIds().get(extUser.getKey()) != null) {
- throw die("username '" + username + "' already exists");
- }
- if (email != null && db.accountExternalIds().get(getEmailKey()) != null) {
- throw die("email '" + email + "' already exists");
- }
-
+ protected void run() throws OrmException, IOException, UnloggedFailure {
+ CreateAccount.Input input = new CreateAccount.Input();
+ input.username = username;
+ input.email = email;
+ input.name = fullName;
+ input.sshKey = readSshKey();
+ input.httpPassword = httpPassword;
+ input.groups = Lists.transform(groups, new Function<AccountGroup.Id, String>() {
+ @Override
+ public String apply(AccountGroup.Id id) {
+ return id.toString();
+ }});
try {
- db.accountExternalIds().insert(Collections.singleton(extUser));
- } catch (OrmDuplicateKeyException duplicateKey) {
- throw die("username '" + username + "' already exists");
+ createAccountFactory.create(username).apply(TopLevelResource.INSTANCE, input);
+ } catch (RestApiException e) {
+ throw die(e.getMessage());
}
-
- if (email != null) {
- AccountExternalId extMailto = new AccountExternalId(id, getEmailKey());
- extMailto.setEmailAddress(email);
- try {
- db.accountExternalIds().insert(Collections.singleton(extMailto));
- } catch (OrmDuplicateKeyException duplicateKey) {
- try {
- db.accountExternalIds().delete(Collections.singleton(extUser));
- } catch (OrmException cleanupError) {
- }
- throw die("email '" + email + "' already exists");
- }
- }
-
- Account a = new Account(id);
- a.setFullName(fullName);
- a.setPreferredEmail(email);
- db.accounts().insert(Collections.singleton(a));
-
- if (key != null) {
- db.accountSshKeys().insert(Collections.singleton(key));
- }
-
- for (AccountGroup.Id groupId : new HashSet<AccountGroup.Id>(groups)) {
- AccountGroupMember m =
- new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
- db.accountGroupMembersAudit().insert(Collections.singleton( //
- new AccountGroupMemberAudit(m, currentUser.getAccountId())));
- db.accountGroupMembers().insert(Collections.singleton(m));
- }
-
- sshKeyCache.evict(username);
- accountCache.evictByUsername(username);
- byEmailCache.evict(email);
}
- private AccountExternalId.Key getEmailKey() {
- return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
- }
-
- private AccountSshKey readSshKey(final Account.Id id)
- throws UnsupportedEncodingException, IOException, InvalidSshKeyException {
+ private String readSshKey() throws UnsupportedEncodingException, IOException {
if (sshKey == null) {
return null;
}
@@ -169,6 +95,6 @@
sshKey += line + "\n";
}
}
- return sshKeyCache.create(new AccountSshKey.Id(id, 1), sshKey.trim());
+ return sshKey;
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 660460a..c652641 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -38,7 +38,7 @@
* Optionally, puts an initial set of user in the newly created group.
*/
@RequiresCapability(GlobalCapability.CREATE_GROUP)
-@CommandMetaData(name = "create-group", descr = "Create a new account group")
+@CommandMetaData(name = "create-group", description = "Create a new account group")
final class CreateGroupCommand extends SshCommand {
@Option(name = "--owner", aliases = {"-o"}, metaVar = "GROUP", usage = "owning group, if not specified the group will be self-owning")
private AccountGroup.Id ownerGroupId;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index bd624ae..24d7689 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -36,7 +36,7 @@
/** Create a new project. **/
@RequiresCapability(GlobalCapability.CREATE_PROJECT)
-@CommandMetaData(name = "create-project", descr = "Create a new project and associated Git repository")
+@CommandMetaData(name = "create-project", description = "Create a new project and associated Git repository")
final class CreateProjectCommand extends SshCommand {
@Option(name = "--name", aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created (deprecated option)")
void setProjectNameFromOption(String name) {
@@ -106,6 +106,9 @@
@Option(name = "--empty-commit", usage = "to create initial empty commit")
private boolean createEmptyCommit;
+ @Option(name = "--max-object-size-limit", usage = "max Git object size for this project")
+ private String maxObjectSizeLimit;
+
private String projectName;
@Argument(index = 0, metaVar = "NAME", usage = "name of project to be created")
@@ -143,6 +146,7 @@
args.changeIdRequired = requireChangeID;
args.branch = branch;
args.createEmptyCommit = createEmptyCommit;
+ args.maxObjectSizeLimit = maxObjectSizeLimit;
final PerformCreateProject createProject = factory.create(args);
createProject.createProject();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index 35a4e14..521103b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -39,6 +39,7 @@
command(gerrit, BanCommitCommand.class);
command(gerrit, FlushCaches.class);
command(gerrit, ListProjectsCommand.class);
+ command(gerrit, ListMembersCommand.class);
command(gerrit, ListGroupsCommand.class);
command(gerrit, LsUserRefs.class);
command(gerrit, Query.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index 13abdf4..6c07dddb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -17,22 +17,21 @@
import com.google.common.cache.Cache;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.sshd.BaseCommand;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import org.kohsuke.args4j.Option;
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
import java.util.SortedSet;
/** Causes the caches to purge all entries and reload. */
@RequiresCapability(GlobalCapability.FLUSH_CACHES)
-@CommandMetaData(name = "flush-caches", descr = "Flush some/all server caches from memory")
+@CommandMetaData(name = "flush-caches", description = "Flush some/all server caches from memory")
final class FlushCaches extends CacheCommand {
private static final String WEB_SESSIONS = "web_sessions";
@@ -98,16 +97,13 @@
private void doBulkFlush() {
try {
- for (String plugin : cacheMap.plugins()) {
- for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
- cacheMap.byPlugin(plugin).entrySet()) {
- String n = cacheNameOf(plugin, entry.getKey());
- if (flush(n)) {
- try {
- entry.getValue().get().invalidateAll();
- } catch (Throwable err) {
- stderr.println("error: cannot flush cache \"" + n + "\": " + err);
- }
+ for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
+ String n = cacheNameOf(e.getPluginName(), e.getExportName());
+ if (flush(n)) {
+ try {
+ e.getProvider().get().invalidateAll();
+ } catch (Throwable err) {
+ stderr.println("error: cannot flush cache \"" + n + "\": " + err);
}
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
index c561153..346bea7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
@@ -37,7 +37,7 @@
/** Runs the Git garbage collection. */
@RequiresCapability(GlobalCapability.RUN_GC)
-@CommandMetaData(name = "gc", descr = "Run Git garbage collection")
+@CommandMetaData(name = "gc", description = "Run Git garbage collection")
public class GarbageCollectionCommand extends BaseCommand {
@Option(name = "--all", usage = "runs the Git garbage collection for all projects")
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index e5b4203..aadb1d9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -37,7 +37,7 @@
import java.io.PrintWriter;
-@CommandMetaData(name = "ls-groups", descr = "List groups visible to the caller")
+@CommandMetaData(name = "ls-groups", description = "List groups visible to the caller")
public class ListGroupsCommand extends BaseCommand {
@Inject
private MyListGroups impl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
new file mode 100644
index 0000000..b7dd380
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupDetailFactory.Factory;
+import com.google.gerrit.server.group.ListMembers;
+import com.google.gerrit.server.ioutil.ColumnFormatter;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gwtorm.server.OrmException;
+
+import org.apache.sshd.server.Environment;
+import org.kohsuke.args4j.Argument;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+import javax.inject.Inject;
+
+/**
+ * Implements a command that allows the user to see the members of a group.
+ */
+@CommandMetaData(name = "ls-members", description = "Lists the members of a given group")
+public class ListMembersCommand extends BaseCommand {
+ @Inject
+ ListMembersCommandImpl impl;
+
+ @Override
+ public void start(Environment env) {
+ startThread(new CommandRunnable() {
+ @Override
+ public void run() throws Exception {
+ parseCommandLine(impl);
+ final PrintWriter stdout = toPrintWriter(out);
+ try {
+ impl.display(stdout);
+ } finally {
+ stdout.flush();
+ }
+ }
+ });
+ }
+
+ private static class ListMembersCommandImpl extends ListMembers {
+ @Argument(required = true, usage = "the name of the group", metaVar = "GROUPNAME")
+ private String name;
+
+ private final GroupCache groupCache;
+
+ @Inject
+ protected ListMembersCommandImpl(GroupCache groupCache,
+ Factory groupDetailFactory,
+ AccountInfo.Loader.Factory accountLoaderFactory,
+ AccountCache accountCache) {
+ super(groupCache, groupDetailFactory, accountLoaderFactory);
+ this.groupCache = groupCache;
+ }
+
+ void display(PrintWriter writer) throws UnloggedFailure, OrmException {
+ AccountGroup group = groupCache.get(new AccountGroup.NameKey(name));
+ String errorText = "Group not found or not visible\n";
+
+ if (group == null) {
+ writer.write(errorText);
+ writer.flush();
+ return;
+ }
+
+ try {
+ List<AccountInfo> members = apply(group.getGroupUUID());
+ ColumnFormatter formatter = new ColumnFormatter(writer, '\t');
+ formatter.addColumn("id");
+ formatter.addColumn("username");
+ formatter.addColumn("full name");
+ formatter.addColumn("email");
+ formatter.nextLine();
+ for (AccountInfo member : members) {
+ if (member == null) {
+ continue;
+ }
+
+ formatter.addColumn(member._id.toString());
+ formatter.addColumn(Objects.firstNonNull(member.username, "n/a"));
+ formatter.addColumn(Objects.firstNonNull(
+ Strings.emptyToNull(member.name), "n/a"));
+ formatter.addColumn(Objects.firstNonNull(member.email, "n/a"));
+ formatter.nextLine();
+ }
+
+ formatter.finish();
+ } catch (MethodNotAllowedException e) {
+ writer.write(errorText);
+ writer.flush();
+ }
+ }
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index ab70395..8bcae4b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -23,7 +23,7 @@
import java.util.List;
-@CommandMetaData(name = "ls-projects", descr = "List projects visible to the caller")
+@CommandMetaData(name = "ls-projects", description = "List projects visible to the caller")
final class ListProjectsCommand extends BaseCommand {
@Inject
private ListProjects impl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index 58abf95..b0ed650 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -40,7 +40,7 @@
import java.util.Map;
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "ls-user-refs", descr = "List refs visible to a specific user")
+@CommandMetaData(name = "ls-user-refs", description = "List refs visible to a specific user")
public class LsUserRefs extends SshCommand {
@Inject
private AccountResolver accountResolver;
@@ -109,7 +109,9 @@
throw new UnloggedFailure("fatal: Error opening: '"
+ projectControl.getProject().getNameKey());
} finally {
- repo.close();
+ if (repo != null) {
+ repo.close();
+ }
}
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
index 30f3c95..abe202e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
@@ -39,6 +39,7 @@
// deprecated alias to review command
alias(gerrit, "approve", ReviewCommand.class);
command(gerrit, SetAccountCommand.class);
+ command(gerrit, SetMembersCommand.class);
command(gerrit, SetProjectCommand.class);
command(gerrit, "test-submit").toProvider(new DispatchCommandProvider(testSubmit));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
index 709e337..47c2d68 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
@@ -28,7 +28,7 @@
import java.util.List;
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "enable", descr = "Enable plugins")
+@CommandMetaData(name = "enable", description = "Enable plugins")
final class PluginEnableCommand extends SshCommand {
@Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin(s) to enable")
List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
index fc036fe..70d09ee 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -35,7 +35,7 @@
import java.net.URL;
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "install", descr = "Install/Add a plugin")
+@CommandMetaData(name = "install", description = "Install/Add a plugin")
final class PluginInstallCommand extends SshCommand {
@Option(name = "--name", aliases = {"-n"}, usage = "install under name")
private String name;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
index ab6c978..7e44641 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -26,7 +26,7 @@
import java.io.IOException;
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "ls", descr = "List the installed plugins")
+@CommandMetaData(name = "ls", description = "List the installed plugins")
final class PluginLsCommand extends BaseCommand {
@Inject
private ListPlugins impl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
index 85ade03..3ed1011 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
@@ -28,7 +28,7 @@
import java.util.List;
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "reload", descr = "Reload/Restart plugins")
+@CommandMetaData(name = "reload", description = "Reload/Restart plugins")
final class PluginReloadCommand extends SshCommand {
@Argument(index = 0, metaVar = "NAME", usage = "plugins to reload/restart")
private List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
index 96adb8fe..0ae11af 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
@@ -27,7 +27,7 @@
import java.util.List;
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "remove", descr = "Disable plugins")
+@CommandMetaData(name = "remove", description = "Disable plugins")
final class PluginRemoveCommand extends SshCommand {
@Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin to remove")
List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
index 2d17876..185bb67 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -24,7 +24,7 @@
import java.util.List;
-@CommandMetaData(name = "query", descr = "Query the change database")
+@CommandMetaData(name = "query", description = "Query the change database")
class Query extends SshCommand {
@Inject
private QueryProcessor processor;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
index 1630d11..3f0bc4c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
@@ -16,6 +16,7 @@
import com.google.gerrit.common.Version;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
@@ -49,7 +50,7 @@
}
public static enum OutputFormat {
- PRETTY, JSON;
+ PRETTY, JSON, JSON_SINGLE;
}
private final BufferedReader in;
@@ -178,6 +179,7 @@
} else {
final String msg = "'\\" + line + "' not supported";
switch (outputFormat) {
+ case JSON_SINGLE:
case JSON: {
final JsonObject err = new JsonObject();
err.addProperty("type", "error");
@@ -228,9 +230,9 @@
if (outputFormat == OutputFormat.PRETTY) {
println(" List of relations");
}
- showResultSet(rs, false, //
- Identity.create(rs, "TABLE_SCHEM"), //
- Identity.create(rs, "TABLE_NAME"), //
+ showResultSet(rs, false, 0,
+ Identity.create(rs, "TABLE_SCHEM"),
+ Identity.create(rs, "TABLE_NAME"),
Identity.create(rs, "TABLE_TYPE"));
} finally {
rs.close();
@@ -267,8 +269,8 @@
if (outputFormat == OutputFormat.PRETTY) {
println(" Table " + tableName);
}
- showResultSet(rs, true, //
- Identity.create(rs, "COLUMN_NAME"), //
+ showResultSet(rs, true, 0,
+ Identity.create(rs, "COLUMN_NAME"),
new Function("TYPE") {
@Override
String apply(final ResultSet rs) throws SQLException {
@@ -365,24 +367,7 @@
if (hasResultSet) {
final ResultSet rs = statement.getResultSet();
try {
- final int rowCount = showResultSet(rs, false);
- final long ms = System.currentTimeMillis() - start;
- switch (outputFormat) {
- case JSON: {
- final JsonObject tail = new JsonObject();
- tail.addProperty("type", "query-stats");
- tail.addProperty("rowCount", rowCount);
- tail.addProperty("runTimeMilliseconds", ms);
- println(tail.toString());
- break;
- }
-
- case PRETTY:
- default:
- println("(" + rowCount + (rowCount == 1 ? " row" : " rows") //
- + "; " + ms + " ms)");
- break;
- }
+ showResultSet(rs, false, start);
} finally {
rs.close();
}
@@ -391,6 +376,7 @@
final int updateCount = statement.getUpdateCount();
final long ms = System.currentTimeMillis() - start;
switch (outputFormat) {
+ case JSON_SINGLE:
case JSON: {
final JsonObject tail = new JsonObject();
tail.addProperty("type", "update-stats");
@@ -411,19 +397,47 @@
}
}
- private int showResultSet(final ResultSet rs, boolean alreadyOnRow,
- Function... show) throws SQLException {
+ /**
+ * Outputs a result set to stdout.
+ *
+ * @param rs ResultSet to show.
+ * @param alreadyOnRow true if rs is already on the first row. false
+ * otherwise.
+ * @param start Timestamp in milliseconds when executing the statement
+ * started. This timestamp is used to compute statistics about the
+ * statement. If no statistics should be shown, set it to 0.
+ * @param show Functions to map columns
+ * @throws SQLException
+ */
+ private void showResultSet(final ResultSet rs, boolean alreadyOnRow,
+ long start, Function... show) throws SQLException {
switch (outputFormat) {
+ case JSON_SINGLE:
case JSON:
- return showResultSetJson(rs, alreadyOnRow, show);
+ showResultSetJson(rs, alreadyOnRow, start, show);
+ break;
case PRETTY:
default:
- return showResultSetPretty(rs, alreadyOnRow, show);
+ showResultSetPretty(rs, alreadyOnRow, start, show);
+ break;
}
}
- private int showResultSetJson(final ResultSet rs, boolean alreadyOnRow,
- Function... show) throws SQLException {
+ /**
+ * Outputs a result set to stdout in Json format.
+ *
+ * @param rs ResultSet to show.
+ * @param alreadyOnRow true if rs is already on the first row. false
+ * otherwise.
+ * @param start Timestamp in milliseconds when executing the statement
+ * started. This timestamp is used to compute statistics about the
+ * statement. If no statistics should be shown, set it to 0.
+ * @param show Functions to map columns
+ * @throws SQLException
+ */
+ private void showResultSetJson(final ResultSet rs, boolean alreadyOnRow,
+ long start, Function... show) throws SQLException {
+ JsonArray collector = new JsonArray();
final ResultSetMetaData meta = rs.getMetaData();
final Function[] columnMap;
if (show != null && 0 < show.length) {
@@ -453,15 +467,68 @@
}
row.addProperty("type", "row");
row.add("columns", cols);
- println(row.toString());
+ switch (outputFormat) {
+ case JSON:
+ println(row.toString());
+ break;
+ case JSON_SINGLE:
+ collector.add(row);
+ break;
+ default:
+ final JsonObject obj = new JsonObject();
+ obj.addProperty("type", "error");
+ obj.addProperty("message", "Unsupported Json variant");
+ println(obj.toString());
+ return;
+ }
alreadyOnRow = false;
rowCnt++;
}
- return rowCnt;
+
+ JsonObject tail = null;
+ if (start != 0) {
+ tail = new JsonObject();
+ tail.addProperty("type", "query-stats");
+ tail.addProperty("rowCount", rowCnt);
+ final long ms = System.currentTimeMillis() - start;
+ tail.addProperty("runTimeMilliseconds", ms);
+ }
+
+ switch (outputFormat) {
+ case JSON:
+ if (tail != null) {
+ println(tail.toString());
+ }
+ break;
+ case JSON_SINGLE:
+ if (tail != null) {
+ collector.add(tail);
+ }
+ println(collector.toString());
+ break;
+ default:
+ final JsonObject obj = new JsonObject();
+ obj.addProperty("type", "error");
+ obj.addProperty("message", "Unsupported Json variant");
+ println(obj.toString());
+ return;
+ }
}
- private int showResultSetPretty(final ResultSet rs, boolean alreadyOnRow,
- Function... show) throws SQLException {
+ /**
+ * Outputs a result set to stdout in plain text format.
+ *
+ * @param rs ResultSet to show.
+ * @param alreadyOnRow true if rs is already on the first row. false
+ * otherwise.
+ * @param start Timestamp in milliseconds when executing the statement
+ * started. This timestamp is used to compute statistics about the
+ * statement. If no statistics should be shown, set it to 0.
+ * @param show Functions to map columns
+ * @throws SQLException
+ */
+ private void showResultSetPretty(final ResultSet rs, boolean alreadyOnRow,
+ long start, Function... show) throws SQLException {
final ResultSetMetaData meta = rs.getMetaData();
final Function[] columnMap;
@@ -559,11 +626,18 @@
if (dataTruncated) {
warning("some column data was truncated");
}
- return rows.size();
+
+ if (start != 0) {
+ final int rowCount = rows.size();
+ final long ms = System.currentTimeMillis() - start;
+ println("(" + rowCount + (rowCount == 1 ? " row" : " rows")
+ + "; " + ms + " ms)");
+ }
}
private void warning(final String msg) {
switch (outputFormat) {
+ case JSON_SINGLE:
case JSON: {
final JsonObject obj = new JsonObject();
obj.addProperty("type", "warning");
@@ -581,6 +655,7 @@
private void error(final SQLException err) {
switch (outputFormat) {
+ case JSON_SINGLE:
case JSON: {
final JsonObject obj = new JsonObject();
obj.addProperty("type", "error");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
index e0dd38f..cd50499 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
@@ -42,7 +42,7 @@
import java.util.Set;
/** Receives change upload over SSH using the Git receive-pack protocol. */
-@CommandMetaData(name = "receive-pack", descr = "Standard Git server side command for client side git push")
+@CommandMetaData(name = "receive-pack", description = "Standard Git server side command for client side git push")
final class Receive extends AbstractGitCommand {
private static final Logger log = LoggerFactory.getLogger(Receive.class);
@@ -94,9 +94,9 @@
final ReceivePack rp = receive.getReceivePack();
rp.setRefLogIdent(currentUser.newRefLogIdent());
rp.setTimeout(config.getTimeout());
- rp.setMaxObjectSizeLimit(config.getMaxObjectSizeLimit());
+ rp.setMaxObjectSizeLimit(config.getEffectiveMaxObjectSizeLimit(
+ projectControl.getProjectState()));
try {
- receive.advertiseHistory();
rp.receive(in, out, err);
} catch (UnpackException badStream) {
// In case this was caused by the user pushing an object whose size
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
index f3c1bb3..38535a4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -25,7 +25,7 @@
import org.kohsuke.args4j.Argument;
-@CommandMetaData(name = "rename-group", descr = "Rename an account group")
+@CommandMetaData(name = "rename-group", description = "Rename an account group")
public class RenameGroupCommand extends SshCommand {
@Argument(index = 0, required = true, metaVar = "GROUP", usage = "name of the group to be renamed")
private String groupName;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 5769a22..7af882e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -31,11 +31,11 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.Abandon;
import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.DeleteDraftPatchSet;
import com.google.gerrit.server.change.PostReview;
import com.google.gerrit.server.change.Restore;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.Submit;
-import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
import com.google.gerrit.server.changedetail.PublishDraft;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.project.ChangeControl;
@@ -63,7 +63,7 @@
import java.util.Map;
import java.util.Set;
-@CommandMetaData(name = "review", descr = "Verify, approve and/or submit one or more patch sets")
+@CommandMetaData(name = "review", description = "Verify, approve and/or submit one or more patch sets")
public class ReviewCommand extends SshCommand {
private static final Logger log =
LoggerFactory.getLogger(ReviewCommand.class);
@@ -105,10 +105,6 @@
@Option(name = "--submit", aliases = "-s", usage = "submit the specified patch set(s)")
private boolean submitChange;
- @Option(name = "--force-message", usage = "publish the message, "
- + "even if the label score cannot be applied due to the change being closed")
- private boolean forceMessage = false;
-
@Option(name = "--publish", usage = "publish the specified draft patch set(s)")
private boolean publishPatchSet;
@@ -135,7 +131,7 @@
private ReviewDb db;
@Inject
- private DeleteDraftPatchSet.Factory deleteDraftPatchSetFactory;
+ private DeleteDraftPatchSet deleteDraftPatchSetImpl;
@Inject
private ProjectControl.Factory projectControlFactory;
@@ -233,6 +229,7 @@
review.labels = Maps.newTreeMap();
review.drafts = PostReview.DraftHandling.PUBLISH;
review.strictLabels = false;
+ review.waitForCommit = true;
for (ApproveOption ao : optionList) {
Short v = ao.value();
if (v != null) {
@@ -288,6 +285,16 @@
new ChangeResource(ctl), patchSet),
input);
}
+
+ if (publishPatchSet) {
+ final ReviewResult result =
+ publishDraftFactory.create(patchSet.getId()).call();
+ handleReviewResultErrors(result);
+ } else if (deleteDraftPatchSet) {
+ deleteDraftPatchSetImpl.apply(new RevisionResource(
+ new ChangeResource(ctl), patchSet),
+ new DeleteDraftPatchSet.Input());
+ }
} catch (InvalidChangeOperationException e) {
throw error(e.getMessage());
} catch (IllegalStateException e) {
@@ -299,16 +306,6 @@
} catch (ResourceConflictException e) {
throw error(e.getMessage());
}
-
- if (publishPatchSet) {
- final ReviewResult result =
- publishDraftFactory.create(patchSet.getId()).call();
- handleReviewResultErrors(result);
- } else if (deleteDraftPatchSet) {
- final ReviewResult result =
- deleteDraftPatchSetFactory.create(patchSet.getId()).call();
- handleReviewResultErrors(result);
- }
}
private void handleReviewResultErrors(final ReviewResult result) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index c938891..5736bb6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -14,32 +14,40 @@
package com.google.gerrit.sshd.commands;
-
-import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.restapi.RawInput;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountException;
-import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AuthRequest;
-import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AddSshKey;
+import com.google.gerrit.server.account.CreateEmail;
+import com.google.gerrit.server.account.DeleteActive;
+import com.google.gerrit.server.account.DeleteEmail;
+import com.google.gerrit.server.account.DeleteSshKey;
+import com.google.gerrit.server.account.GetEmails;
+import com.google.gerrit.server.account.GetEmails.EmailInfo;
+import com.google.gerrit.server.account.GetSshKeys;
+import com.google.gerrit.server.account.GetSshKeys.SshKeyInfo;
+import com.google.gerrit.server.account.PutActive;
+import com.google.gerrit.server.account.PutHttpPassword;
+import com.google.gerrit.server.account.PutName;
import com.google.gerrit.sshd.BaseCommand;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
@@ -47,7 +55,7 @@
import java.util.List;
/** Set a user's account settings. **/
-@CommandMetaData(name = "set-account", descr = "Change an account's settings")
+@CommandMetaData(name = "set-account", description = "Change an account's settings")
final class SetAccountCommand extends BaseCommand {
@Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id")
@@ -81,19 +89,40 @@
private IdentifiedUser currentUser;
@Inject
- private ReviewDb db;
+ private IdentifiedUser.GenericFactory genericUserFactory;
@Inject
- private AccountManager manager;
+ private CreateEmail.Factory createEmailFactory;
@Inject
- private SshKeyCache sshKeyCache;
+ private Provider<GetEmails> getEmailsProvider;
@Inject
- private AccountCache byIdCache;
+ private Provider<DeleteEmail> deleteEmailProvider;
@Inject
- private Realm realm;
+ private Provider<PutName> putNameProvider;
+
+ @Inject
+ private Provider<PutHttpPassword> putHttpPasswordProvider;
+
+ @Inject
+ private Provider<PutActive> putActiveProvider;
+
+ @Inject
+ private Provider<DeleteActive> deleteActiveProvider;
+
+ @Inject
+ private Provider<AddSshKey> addSshKeyProvider;
+
+ @Inject
+ private Provider<GetSshKeys> getSshKeysProvider;
+
+ @Inject
+ private Provider<DeleteSshKey> deleteSshKeyProvider;
+
+ private IdentifiedUser user;
+ private AccountResource rsrc;
@Override
public void start(final Environment env) {
@@ -131,151 +160,127 @@
}
private void setAccount() throws OrmException, IOException, UnloggedFailure {
-
- final Account account = db.accounts().get(id);
- boolean accountUpdated = false;
- boolean sshKeysUpdated = false;
-
- ResultSet<AccountExternalId> ids = db.accountExternalIds().byAccount(id);
- for (AccountExternalId extId : ids) {
- if (extId.isScheme(AccountExternalId.SCHEME_USERNAME)) {
- account.setUserName(extId.getSchemeRest());
+ user = genericUserFactory.create(id);
+ rsrc = new AccountResource(user);
+ try {
+ for (String email : addEmails) {
+ addEmail(email);
}
- }
- for (String email : addEmails) {
- link(id, email);
- }
-
- for (String email : deleteEmails) {
- deleteMail(id, email);
- }
-
- if (fullName != null) {
- if (realm.allowsEdit(FieldName.FULL_NAME)) {
- account.setFullName(fullName);
- accountUpdated = true;
- } else {
- throw new UnloggedFailure(1, "The realm doesn't allow editing names");
+ for (String email : deleteEmails) {
+ deleteEmail(email);
}
- }
- if (httpPassword != null) {
- setHttpPassword(id, httpPassword);
- }
+ if (fullName != null) {
+ PutName.Input in = new PutName.Input();
+ in.name = fullName;
+ putNameProvider.get().apply(rsrc, in);
+ }
- if (active) {
- accountUpdated = true;
- account.setActive(true);
- } else if (inactive) {
- accountUpdated = true;
- account.setActive(false);
- }
+ if (httpPassword != null) {
+ PutHttpPassword.Input in = new PutHttpPassword.Input();
+ in.httpPassword = httpPassword;
+ putHttpPasswordProvider.get().apply(rsrc, in);
+ }
- addSshKeys = readSshKey(addSshKeys);
- if (!addSshKeys.isEmpty()) {
- sshKeysUpdated = true;
- addSshKeys(addSshKeys, account);
- }
+ if (active) {
+ putActiveProvider.get().apply(rsrc, null);
+ } else if (inactive) {
+ try {
+ deleteActiveProvider.get().apply(rsrc, null);
+ } catch (ResourceNotFoundException e) {
+ // user is already inactive
+ }
+ }
- deleteSshKeys = readSshKey(deleteSshKeys);
- if (!deleteSshKeys.isEmpty()) {
- sshKeysUpdated = true;
- deleteSshKeys(deleteSshKeys, account);
- }
+ addSshKeys = readSshKey(addSshKeys);
+ if (!addSshKeys.isEmpty()) {
+ addSshKeys(addSshKeys);
+ }
- if (accountUpdated) {
- db.accounts().update(Collections.singleton(account));
- byIdCache.evict(id);
- }
-
- if (sshKeysUpdated) {
- sshKeyCache.evict(account.getUserName());
+ deleteSshKeys = readSshKey(deleteSshKeys);
+ if (!deleteSshKeys.isEmpty()) {
+ deleteSshKeys(deleteSshKeys);
+ }
+ } catch (RestApiException e) {
+ throw die(e.getMessage());
}
}
- private void addSshKeys(final List<String> keys, final Account account)
- throws OrmException, UnloggedFailure {
- List<AccountSshKey> accountKeys = new ArrayList<AccountSshKey>();
- int seq = db.accountSshKeys().byAccount(account.getId()).toList().size();
- for (String key : keys) {
- try {
- seq++;
- AccountSshKey accountSshKey = sshKeyCache.create(
- new AccountSshKey.Id(account.getId(), seq), key.trim());
- accountKeys.add(accountSshKey);
- } catch (InvalidSshKeyException e) {
- throw new UnloggedFailure(1, "fatal: invalid ssh key");
- }
+ private void addSshKeys(List<String> sshKeys) throws RestApiException,
+ UnloggedFailure, OrmException, IOException {
+ for (final String sshKey : sshKeys) {
+ AddSshKey.Input in = new AddSshKey.Input();
+ in.raw = new RawInput() {
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return new ByteArrayInputStream(sshKey.getBytes("UTF-8"));
+ }
+
+ @Override
+ public String getContentType() {
+ return "plain/text";
+ }
+
+ @Override
+ public long getContentLength() {
+ return sshKey.length();
+ }
+ };
+ addSshKeyProvider.get().apply(rsrc, in);
}
- db.accountSshKeys().insert(accountKeys);
}
- private void deleteSshKeys(final List<String> keys, final Account account)
- throws OrmException {
- ResultSet<AccountSshKey> allKeys = db.accountSshKeys().byAccount(account.getId());
- if (keys.contains("ALL")) {
- db.accountSshKeys().delete(allKeys);
+ private void deleteSshKeys(List<String> sshKeys) throws RestApiException,
+ OrmException {
+ List<SshKeyInfo> infos = getSshKeysProvider.get().apply(rsrc);
+ if (sshKeys.contains("ALL")) {
+ for (SshKeyInfo i : infos) {
+ deleteSshKey(i);
+ }
} else {
- List<AccountSshKey> accountKeys = new ArrayList<AccountSshKey>();
- for (String key : keys) {
- for (AccountSshKey accountSshKey : allKeys) {
- if (key.trim().equals(accountSshKey.getSshPublicKey())
- || accountSshKey.getComment().trim().equals(key)) {
- accountKeys.add(accountSshKey);
+ for (String sshKey : sshKeys) {
+ for (SshKeyInfo i : infos) {
+ if (sshKey.trim().equals(i.sshPublicKey)
+ || sshKey.trim().equals(i.comment)) {
+ deleteSshKey(i);
}
}
}
- db.accountSshKeys().delete(accountKeys);
}
}
- private void deleteMail(Account.Id id, final String mailAddress)
- throws UnloggedFailure, OrmException {
- if (mailAddress.equals("ALL")) {
- ResultSet<AccountExternalId> ids = db.accountExternalIds().byAccount(id);
- for (AccountExternalId extId : ids) {
- if (extId.isScheme(AccountExternalId.SCHEME_MAILTO)) {
- unlink(id, extId.getEmailAddress());
- }
+ private void deleteSshKey(SshKeyInfo i) throws OrmException {
+ AccountSshKey sshKey = new AccountSshKey(
+ new AccountSshKey.Id(user.getAccountId(), i.seq), i.sshPublicKey);
+ deleteSshKeyProvider.get().apply(
+ new AccountResource.SshKey(user, sshKey), null);
+ }
+
+ private void addEmail(String email) throws UnloggedFailure, RestApiException,
+ OrmException {
+ CreateEmail.Input in = new CreateEmail.Input();
+ in.email = email;
+ in.noConfirmation = true;
+ try {
+ createEmailFactory.create(email).apply(rsrc, in);
+ } catch (EmailException e) {
+ throw die(e.getMessage());
+ }
+ }
+
+ private void deleteEmail(String email) throws UnloggedFailure,
+ RestApiException, OrmException {
+ if (email.equals("ALL")) {
+ List<EmailInfo> emails = getEmailsProvider.get().apply(rsrc);
+ DeleteEmail deleteEmail = deleteEmailProvider.get();
+ for (EmailInfo e : emails) {
+ deleteEmail.apply(new AccountResource.Email(user, e.email),
+ new DeleteEmail.Input());
}
} else {
- AccountExternalId.Key key = new AccountExternalId.Key(
- AccountExternalId.SCHEME_MAILTO, mailAddress);
- AccountExternalId extId = db.accountExternalIds().get(key);
- if (extId != null) {
- unlink(id, mailAddress);
- }
- }
- }
-
- private void setHttpPassword(Account.Id id, final String httpPassword)
- throws UnloggedFailure, OrmException {
- ResultSet<AccountExternalId> ids = db.accountExternalIds().byAccount(id);
- for (AccountExternalId extId: ids) {
- if (extId.isScheme(AccountExternalId.SCHEME_USERNAME)) {
- extId.setPassword(httpPassword);
- db.accountExternalIds().update(Collections.singleton(extId));
- byIdCache.evict(id);
- }
- }
- }
-
- private void unlink(Account.Id id, final String mailAddress)
- throws UnloggedFailure {
- try {
- manager.unlink(id, AuthRequest.forEmail(mailAddress));
- } catch (AccountException ex) {
- throw die(ex.getMessage());
- }
- }
-
- private void link(Account.Id id, final String mailAddress)
- throws UnloggedFailure {
- try {
- manager.link(id, AuthRequest.forEmail(mailAddress));
- } catch (AccountException ex) {
- throw die(ex.getMessage());
+ deleteEmailProvider.get().apply(new AccountResource.Email(user, email),
+ new DeleteEmail.Input());
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
new file mode 100644
index 0000000..48c37b8
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
@@ -0,0 +1,164 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.group.AddIncludedGroups;
+import com.google.gerrit.server.group.AddMembers;
+import com.google.gerrit.server.group.DeleteIncludedGroups;
+import com.google.gerrit.server.group.DeleteMembers;
+import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+
+@CommandMetaData(name = "set-members", description = "Modifies members of specific group or number of groups")
+public class SetMembersCommand extends SshCommand {
+
+ @Option(name = "--add", aliases = {"-a"}, metaVar = "USER", usage = "users that should be added as group member")
+ private List<Account.Id> accountsToAdd = Lists.newArrayList();
+
+ @Option(name = "--remove", aliases = {"-r"}, metaVar = "USER", usage = "users that should be removed from the group")
+ private List<Account.Id> accountsToRemove = Lists.newArrayList();
+
+ @Option(name = "--include", aliases = {"-i"}, metaVar = "GROUP", usage = "group that should be included as group member")
+ private List<AccountGroup.UUID> groupsToInclude = Lists.newArrayList();
+
+ @Option(name = "--exclude", aliases = {"-e"}, metaVar = "GROUP", usage = "group that should be excluded from the group")
+ private List<AccountGroup.UUID> groupsToRemove = Lists.newArrayList();
+
+ @Argument(index = 0, required = true, multiValued = true, metaVar = "GROUP", usage = "groups to modify")
+ private List<AccountGroup.UUID> groups = Lists.newArrayList();
+
+ @Inject
+ private Provider<AddMembers> addMembers;
+
+ @Inject
+ private Provider<DeleteMembers> deleteMembers;
+
+ @Inject
+ private Provider<AddIncludedGroups> addIncludedGroups;
+
+ @Inject
+ private Provider<DeleteIncludedGroups> deleteIncludedGroups;
+
+ @Inject
+ private GroupsCollection groupsCollection;
+
+ @Inject
+ private GroupCache groupCache;
+
+ @Inject
+ private AccountCache accountCache;
+
+ @Override
+ protected void run() throws UnloggedFailure, Failure, Exception {
+ for (AccountGroup.UUID groupUuid : groups) {
+ GroupResource resource =
+ groupsCollection.parse(TopLevelResource.INSTANCE,
+ IdString.fromUrl(groupUuid.get()));
+ if (!accountsToRemove.isEmpty()) {
+ deleteMembers.get().apply(resource, fromMembers(accountsToRemove));
+ reportMembersAction("removed from", resource, accountsToRemove);
+ }
+ if (!groupsToRemove.isEmpty()) {
+ deleteIncludedGroups.get().apply(resource, fromGroups(groupsToRemove));
+ reportGroupsAction("excluded from", resource, groupsToRemove);
+ }
+ if (!accountsToAdd.isEmpty()) {
+ addMembers.get().apply(resource, fromMembers(accountsToAdd));
+ reportMembersAction("added to", resource, accountsToAdd);
+ }
+ if (!groupsToInclude.isEmpty()) {
+ addIncludedGroups.get().apply(resource, fromGroups(groupsToInclude));
+ reportGroupsAction("included to", resource, groupsToInclude);
+ }
+ }
+ }
+
+ private void reportMembersAction(String action, GroupResource group,
+ List<Account.Id> accountIdList) throws UnsupportedEncodingException,
+ IOException {
+ out.write(String.format(
+ "Members %s group %s: %s\n",
+ action,
+ group.getName(),
+ Joiner.on(", ").join(
+ Iterables.transform(accountIdList,
+ new Function<Account.Id, String>() {
+ @Override
+ public String apply(Account.Id accountId) {
+ return Objects.firstNonNull(accountCache.get(accountId)
+ .getAccount().getPreferredEmail(), "n/a");
+ }
+ }))).getBytes(ENC));
+ }
+
+ private void reportGroupsAction(String action, GroupResource group,
+ List<AccountGroup.UUID> groupUuidList)
+ throws UnsupportedEncodingException, IOException {
+ out.write(String.format(
+ "Groups %s group %s: %s\n",
+ action,
+ group.getName(),
+ Joiner.on(", ").join(
+ Iterables.transform(groupUuidList,
+ new Function<AccountGroup.UUID, String>() {
+ @Override
+ public String apply(AccountGroup.UUID uuid) {
+ return groupCache.get(uuid).getName();
+ }
+ }))).getBytes(ENC));
+ }
+
+ private AddIncludedGroups.Input fromGroups(List<AccountGroup.UUID> accounts) {
+ return AddIncludedGroups.Input.fromGroups(Lists.newArrayList(Iterables
+ .transform(accounts, new Function<AccountGroup.UUID, String>() {
+ @Override
+ public String apply(AccountGroup.UUID uuid) {
+ return uuid.toString();
+ }
+ })));
+ }
+
+ private AddMembers.Input fromMembers(List<Account.Id> accounts) {
+ return AddMembers.Input.fromMembers(Lists.newArrayList(Iterables.transform(
+ accounts, new Function<Account.Id, String>() {
+ @Override
+ public String apply(Account.Id id) {
+ return id.toString();
+ }
+ })));
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index 8c06c97d..b45fb3a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -38,7 +38,7 @@
import java.io.IOException;
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "set-project", descr = "Change a project's settings")
+@CommandMetaData(name = "set-project", description = "Change a project's settings")
final class SetProjectCommand extends SshCommand {
private static final Logger log = LoggerFactory
.getLogger(SetProjectCommand.class);
@@ -108,6 +108,9 @@
@Option(name = "--project-state", aliases = {"--ps"}, usage = "project's visibility state")
private State state;
+ @Option(name = "--max-object-size-limit", usage = "max Git object size for this project")
+ private String maxObjectSizeLimit;
+
@Inject
private MetaDataUpdate.User metaDataUpdateFactory;
@@ -148,6 +151,9 @@
if (state != null) {
project.setState(state);
}
+ if (maxObjectSizeLimit != null) {
+ project.setMaxObjectSizeLimit(maxObjectSizeLimit);
+ }
md.setMessage("Project settings updated");
config.commit(md);
} finally {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 8e9bcac..6dc79ff 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -45,7 +45,7 @@
import java.util.List;
import java.util.Set;
-@CommandMetaData(name = "set-reviewers", descr = "Add or remove reviewers on a change")
+@CommandMetaData(name = "set-reviewers", description = "Add or remove reviewers on a change")
public class SetReviewersCommand extends SshCommand {
private static final Logger log =
LoggerFactory.getLogger(SetReviewersCommand.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 3366841..b02d9c8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -21,6 +21,7 @@
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.server.cache.h2.H2CacheImpl;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.git.WorkQueue;
@@ -51,7 +52,7 @@
/** Show the current cache states. */
@RequiresCapability(GlobalCapability.VIEW_CACHES)
-@CommandMetaData(name = "show-caches", descr = "Display current cache statistics")
+@CommandMetaData(name = "show-caches", description = "Display current cache statistics")
final class ShowCaches extends CacheCommand {
private static volatile long serverStarted;
@@ -208,13 +209,10 @@
private Map<String, Cache<?, ?>> sortedPluginCaches() {
SortedMap<String, Cache<?, ?>> m = Maps.newTreeMap();
- for (String plugin : cacheMap.plugins()) {
- if ("gerrit".equals(plugin)) {
- continue;
- }
- for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
- cacheMap.byPlugin(plugin).entrySet()) {
- m.put(cacheNameOf(plugin, entry.getKey()), entry.getValue().get());
+ for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
+ if (!"gerrit".equals(e.getPluginName())) {
+ m.put(cacheNameOf(e.getPluginName(), e.getExportName()),
+ e.getProvider().get());
}
}
return m;
@@ -292,7 +290,7 @@
runtimeBean.getVmVendor(),
runtimeBean.getVmName(),
runtimeBean.getVmVersion());
- stdout.format(" on %s %s %s\n", "",
+ stdout.format(" on %s %s %s\n",
osBean.getName(),
osBean.getVersion(),
osBean.getArch());
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
index 1c3d828..c0e5d6e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -27,9 +27,11 @@
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.session.IoSession;
+import org.apache.sshd.server.Environment;
import org.apache.sshd.server.session.ServerSession;
import org.kohsuke.args4j.Option;
+import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
@@ -42,14 +44,33 @@
/** Show the current SSH connections. */
@RequiresCapability(GlobalCapability.VIEW_CONNECTIONS)
-@CommandMetaData(name = "show-connections", descr = "Display active client SSH connections")
+@CommandMetaData(name = "show-connections", description = "Display active client SSH connections")
final class ShowConnections extends SshCommand {
@Option(name = "--numeric", aliases = {"-n"}, usage = "don't resolve names")
private boolean numeric;
+ @Option(name = "--wide", aliases = {"-w"}, usage = "display without line width truncation")
+ private boolean wide;
+
@Inject
private SshDaemon daemon;
+ private int hostNameWidth;
+ private int columns = 80;
+
+ @Override
+ public void start(final Environment env) throws IOException {
+ String s = env.getEnv().get(Environment.ENV_COLUMNS);
+ if (s != null && !s.isEmpty()) {
+ try {
+ columns = Integer.parseInt(s);
+ } catch (NumberFormatException err) {
+ columns = 80;
+ }
+ }
+ super.start(env);
+ }
+
@Override
protected void run() throws Failure {
final IoAcceptor acceptor = daemon.getIoAcceptor();
@@ -71,6 +92,8 @@
}
});
+ hostNameWidth = wide ? Integer.MAX_VALUE : columns - 9 - 9 - 10 - 32;
+
final long now = System.currentTimeMillis();
stdout.print(String.format("%-8s %8s %8s %-15s %s\n", //
"Session", "Start", "Idle", "User", "Remote Host"));
@@ -83,7 +106,7 @@
final long start = io.getCreationTime();
final long idle = now - io.getLastIoTime();
- stdout.print(String.format("%8s %8s %8s %-15.15s %.30s\n", //
+ stdout.print(String.format("%8s %8s %8s %-15.15s %s\n", //
id(sd), //
time(now, start), //
age(idle), //
@@ -123,7 +146,7 @@
}
final CurrentUser user = sd.getCurrentUser();
- if (user instanceof IdentifiedUser) {
+ if (user != null && user.isIdentifiedUser()) {
IdentifiedUser u = (IdentifiedUser) user;
if (!numeric) {
@@ -160,6 +183,11 @@
if (host == null) {
host = remoteAddress.toString();
}
+
+ if (host.length() > hostNameWidth) {
+ return host.substring(0, hostNameWidth);
+ }
+
return host;
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index fee5275..2f1d0bf 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -40,9 +40,9 @@
/** Display the current work queue. */
@AdminHighPriorityCommand
-@CommandMetaData(name = "show-queue", descr = "Display the background work queues, including replication")
+@CommandMetaData(name = "show-queue", description = "Display the background work queues, including replication")
final class ShowQueue extends SshCommand {
- @Option(name = "-w", usage = "display without line width truncation")
+ @Option(name = "--wide", aliases = {"-w"}, usage = "display without line width truncation")
private boolean wide;
@Inject
@@ -94,10 +94,10 @@
}
});
- taskNameWidth = wide ? Integer.MAX_VALUE : columns - 8 - 12 - 8 - 4;
+ taskNameWidth = wide ? Integer.MAX_VALUE : columns - 8 - 12 - 12 - 4 - 4;
- stdout.print(String.format("%-8s %-12s %-8s %s\n", //
- "Task", "State", "", "Command"));
+ stdout.print(String.format("%-8s %-12s %-12s %-4s %s\n", //
+ "Task", "State", "StartTime", "", "Command"));
stdout.print("----------------------------------------------"
+ "--------------------------------\n");
@@ -149,10 +149,12 @@
}
}
+ String startTime = startTime(task.getStartTime());
+
// Shows information about tasks depending on the user rights
if (viewAll || (!hasCustomizedPrint && regularUserCanSee)) {
- stdout.print(String.format("%8s %-12s %-8s %s\n", //
- id(task.getTaskId()), start, "", format(task)));
+ stdout.print(String.format("%8s %-12s %-12s %-4s %s\n", //
+ id(task.getTaskId()), start, startTime, "", format(task)));
} else if (regularUserCanSee) {
if (remoteName == null) {
remoteName = projectName.get();
@@ -160,8 +162,8 @@
remoteName = remoteName + "/" + projectName;
}
- stdout.print(String.format("%8s %-12s %-8s %s\n", //
- id(task.getTaskId()), start, "", remoteName));
+ stdout.print(String.format("%8s %-12s %-4s %s\n", //
+ id(task.getTaskId()), start, startTime, remoteName));
}
}
stdout.print("----------------------------------------------"
@@ -180,7 +182,15 @@
private static String time(final long now, final long delay) {
final Date when = new Date(now + delay);
- if (delay < 24 * 60 * 60 * 1000L) {
+ return format(when, delay);
+ }
+
+ private static String startTime(final Date when) {
+ return format(when, System.currentTimeMillis() - when.getTime());
+ }
+
+ private static String format(final Date when, final long timeFromNow) {
+ if (timeFromNow < 24 * 60 * 60 * 1000L) {
return new SimpleDateFormat("HH:mm:ss.SSS").format(when);
}
return new SimpleDateFormat("MMM-dd HH:mm").format(when);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
index 99d4baa..fd4a9ec 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -36,7 +36,7 @@
import java.util.concurrent.LinkedBlockingQueue;
@RequiresCapability(GlobalCapability.STREAM_EVENTS)
-@CommandMetaData(name = "stream-events", descr = "Monitor events occurring in real time")
+@CommandMetaData(name = "stream-events", description = "Monitor events occurring in real time")
final class StreamEvents extends BaseCommand {
/** Maximum number of events that may be queued up for each connection. */
private static final int MAX_EVENTS = 128;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
index 6335160..b957a7a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
@@ -23,7 +23,7 @@
import com.google.inject.Provider;
/** Command that allows testing of prolog submit-rules in a live instance. */
-@CommandMetaData(name = "rule", descr = "Test prolog submit rules")
+@CommandMetaData(name = "rule", description = "Test prolog submit rules")
final class TestSubmitRuleCommand extends BaseTestPrologCommand {
@Inject
private Provider<TestSubmitRule> view;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
index 326ff46..2e7f0df 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
@@ -23,7 +23,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
-@CommandMetaData(name = "type", descr = "Test prolog submit type")
+@CommandMetaData(name = "type", description = "Test prolog submit type")
final class TestSubmitTypeCommand extends BaseTestPrologCommand {
@Inject
private Provider<TestSubmitType> view;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
index 2066cc2..19888c8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
@@ -18,7 +18,7 @@
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
-@CommandMetaData(name = "version", descr = "Display gerrit version")
+@CommandMetaData(name = "version", description = "Display gerrit version")
final class VersionCommand extends SshCommand {
@Override
diff --git a/gerrit-util-cli/BUCK b/gerrit-util-cli/BUCK
new file mode 100644
index 0000000..d5fff90
--- /dev/null
+++ b/gerrit-util-cli/BUCK
@@ -0,0 +1,12 @@
+java_library(
+ name = 'cli',
+ srcs = glob(['src/main/java/**/*.java']),
+ deps = [
+ '//lib:args4j',
+ '//lib:guava',
+ '//lib:jsr305',
+ '//lib/guice:guice',
+ '//lib/guice:guice-assistedinject',
+ ],
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
deleted file mode 100644
index 9a897c7..0000000
--- a/gerrit-util-cli/pom.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-util-cli</artifactId>
- <name>Gerrit Code Review - Utility - CLI</name>
-
- <description>
- Utilities to support command line parsing
- </description>
-
- <dependencies>
- <dependency>
- <groupId>args4j</groupId>
- <artifactId>args4j</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.inject</groupId>
- <artifactId>guice</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.inject.extensions</groupId>
- <artifactId>guice-assistedinject</artifactId>
- </dependency>
- </dependencies>
-</project>
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index 69598ce..b75635f 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -34,13 +34,12 @@
package com.google.gerrit.util.cli;
+import com.google.common.base.Strings;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Key;
import com.google.inject.assistedinject.Assisted;
import org.kohsuke.args4j.Argument;
@@ -75,7 +74,7 @@
CmdLineParser create(Object bean);
}
- private final Injector injector;
+ private final OptionHandlers handlers;
private final MyParser parser;
@SuppressWarnings("rawtypes")
@@ -94,9 +93,9 @@
* annotations incorrectly.
*/
@Inject
- public CmdLineParser(final Injector injector, @Assisted final Object bean)
+ public CmdLineParser(OptionHandlers handlers, @Assisted final Object bean)
throws IllegalAnnotationError {
- this.injector = injector;
+ this.handlers = handlers;
this.parser = new MyParser(bean);
}
@@ -153,12 +152,7 @@
}
out.write('=');
- String var = handler.getDefaultMetaVariable();
- if (handler instanceof EnumOptionHandler) {
- var = var.substring(1, var.length() - 1);
- var = var.replaceAll(" ", "");
- }
- out.write(var);
+ out.write(metaVar(handler, n));
if (!n.required()) {
out.write(']');
}
@@ -186,6 +180,17 @@
}
}
+ private static String metaVar(OptionHandler<?> handler, NamedOptionDef n) {
+ String var = n.metaVar();
+ if (Strings.isNullOrEmpty(var)) {
+ var = handler.getDefaultMetaVariable();
+ if (handler instanceof EnumOptionHandler) {
+ var = var.substring(1, var.length() - 1).replace(" ", "");
+ }
+ }
+ return var;
+ }
+
public boolean wasHelpRequestedByOption() {
return parser.help.value;
}
@@ -327,16 +332,10 @@
return add(super.createOptionHandler(option, setter));
}
- final Key<OptionHandlerFactory<?>> key =
- OptionHandlerUtil.keyFor(setter.getType());
- Injector i = injector;
- while (i != null) {
- if (i.getBindings().containsKey(key)) {
- return add(i.getInstance(key).create(this, option, setter));
- }
- i = i.getParent();
+ OptionHandlerFactory<?> factory = handlers.get(setter.getType());
+ if (factory != null) {
+ return factory.create(this, option, setter);
}
-
return add(super.createOptionHandler(option, setter));
}
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlers.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlers.java
new file mode 100644
index 0000000..5cf591c
--- /dev/null
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlers.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.util.cli;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+
+import java.lang.reflect.ParameterizedType;
+import java.util.Map.Entry;
+
+import javax.annotation.Nullable;
+
+@Singleton
+public class OptionHandlers {
+ public static OptionHandlers empty() {
+ ImmutableMap<Class<?>, Provider<OptionHandlerFactory<?>>> m =
+ ImmutableMap.of();
+ return new OptionHandlers(m);
+ }
+
+ private final ImmutableMap<Class<?>, Provider<OptionHandlerFactory<?>>> map;
+
+ @Inject
+ OptionHandlers(Injector parent) {
+ this(build(parent));
+ }
+
+ OptionHandlers(ImmutableMap<Class<?>, Provider<OptionHandlerFactory<?>>> m) {
+ this.map = m;
+ }
+
+ @Nullable
+ OptionHandlerFactory<?> get(Class<?> type) {
+ Provider<OptionHandlerFactory<?>> b = map.get(type);
+ return b != null ? b.get() : null;
+ }
+
+ private static ImmutableMap<Class<?>, Provider<OptionHandlerFactory<?>>> build(Injector i) {
+ ImmutableMap.Builder<Class<?>, Provider<OptionHandlerFactory<?>>> map =
+ ImmutableMap.builder();
+ for (; i != null; i = i.getParent()) {
+ for (Entry<Key<?>, Binding<?>> e : i.getBindings().entrySet()) {
+ TypeLiteral<?> type = e.getKey().getTypeLiteral();
+ if (type.getRawType() == OptionHandlerFactory.class
+ && e.getKey().getAnnotation() == null
+ && type.getType() instanceof ParameterizedType) {
+ map.put(getType(type), cast(e.getValue()).getProvider());
+ }
+ }
+ }
+ return map.build();
+ }
+
+ private static Class<?> getType(TypeLiteral<?> t) {
+ ParameterizedType p = (ParameterizedType) t.getType();
+ return (Class<?>) p.getActualTypeArguments()[0];
+ }
+
+ private static Binding<OptionHandlerFactory<?>> cast(Binding<?> e) {
+ @SuppressWarnings("unchecked")
+ Binding<OptionHandlerFactory<?>> b = (Binding<OptionHandlerFactory<?>>) e;
+ return b;
+ }
+}
diff --git a/gerrit-util-ssl/BUCK b/gerrit-util-ssl/BUCK
new file mode 100644
index 0000000..068f34c
--- /dev/null
+++ b/gerrit-util-ssl/BUCK
@@ -0,0 +1,5 @@
+java_library(
+ name = 'ssl',
+ srcs = glob(['src/main/java/**/*.java']),
+ visibility = ['PUBLIC'],
+)
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
deleted file mode 100644
index e687912..0000000
--- a/gerrit-util-ssl/pom.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-util-ssl</artifactId>
- <name>Gerrit Code Review - Utility - SSL</name>
-
- <description>
- Utilities to support SSL based protocols
- </description>
-</project>
diff --git a/gerrit-war/BUCK b/gerrit-war/BUCK
new file mode 100644
index 0000000..03461b8
--- /dev/null
+++ b/gerrit-war/BUCK
@@ -0,0 +1,73 @@
+include_defs('//tools/git.defs')
+
+java_library2(
+ name = 'init',
+ srcs = glob(['src/main/java/**/*.java']),
+ deps = [
+ '//gerrit-cache-h2:cache-h2',
+ '//gerrit-extension-api:api',
+ '//gerrit-httpd:httpd',
+ '//gerrit-lucene:lucene',
+ '//gerrit-openid:openid',
+ '//gerrit-reviewdb:server',
+ '//gerrit-server:server',
+ '//gerrit-server/src/main/prolog:common',
+ '//gerrit-solr:solr',
+ '//gerrit-sshd:sshd',
+ '//lib:gwtorm',
+ '//lib/guice:guice',
+ '//lib/guice:guice-servlet',
+ '//lib/log:api',
+ '//lib/jgit:jgit',
+ ],
+ compile_deps = ['//lib:servlet-api-3_0'],
+ visibility = [
+ '//:',
+ '//gerrit-gwtdebug:gwtdebug',
+ '//tools/eclipse:classpath',
+ ],
+)
+
+genrule(
+ name = 'webapp_assets',
+ cmd = 'cd $SRCDIR/src/main/webapp; zip -qr $OUT .',
+ srcs = glob(['src/main/webapp/**/*']),
+ deps = [],
+ out = 'webapp_assets.zip',
+ visibility = ['//:'],
+)
+
+genrule(
+ name = 'log4j-config__jar',
+ cmd = 'jar cf $OUT -C $SRCDIR/src/main/resources .',
+ srcs = ['src/main/resources/log4j.properties'],
+ out = 'log4j-config.jar',
+)
+
+prebuilt_jar(
+ name = 'log4j-config',
+ binary_jar = genfile('log4j-config.jar'),
+ deps = [':log4j-config__jar'],
+ visibility = [
+ '//:',
+ '//tools/eclipse:classpath',
+ ],
+)
+
+prebuilt_jar(
+ name = 'version',
+ binary_jar = genfile('version.jar'),
+ deps = [':gen_version'],
+ visibility = ['//:'],
+)
+
+genrule(
+ name = 'gen_version',
+ cmd = ';'.join([
+ 'cd $TMP',
+ 'mkdir -p com/google/gerrit/common',
+ 'echo "%s" >com/google/gerrit/common/Version' % git_describe(),
+ 'zip -9Dqr $OUT .',
+ ]),
+ out = 'version.jar',
+)
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
deleted file mode 100644
index b6c90e1..0000000
--- a/gerrit-war/pom.xml
+++ /dev/null
@@ -1,344 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2009 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <parent>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <version>2.7</version>
- </parent>
-
- <artifactId>gerrit-war</artifactId>
- <name>Gerrit Code Review - WAR</name>
- <packaging>war</packaging>
-
- <description>
- Gerrit packaged as a standard web application archive
- </description>
-
- <dependencies>
- <dependency>
- <groupId>org.apache.tomcat</groupId>
- <artifactId>tomcat-servlet-api</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-gwtui</artifactId>
- <version>${project.version}</version>
- <type>war</type>
- <scope>runtime</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-main</artifactId>
- <version>${project.version}</version>
- <scope>runtime</scope>
- </dependency>
-
- <dependency>
- <groupId>bouncycastle</groupId>
- <artifactId>bcprov-jdk15</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>bouncycastle</groupId>
- <artifactId>bcpg-jdk15</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-log4j12</artifactId>
- </dependency>
-
- <dependency>
- <groupId>log4j</groupId>
- <artifactId>log4j</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-openid</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-sshd</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-httpd</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-pgm</artifactId>
- <version>${project.version}</version>
- <exclusions>
- <exclusion>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-servlet</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-servlet</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>org.apache.tomcat</groupId>
- <artifactId>servlet-api</artifactId>
- <scope>provided</scope>
- </dependency>
- </dependencies>
-
- <profiles>
- <profile>
- <id>plugins</id>
- <activation>
- <property>
- <name>!gerrit.plugins.skip</name>
- </property>
- </activation>
- <dependencies>
- <!-- CORE PLUGIN LIST -->
- <dependency>
- <groupId>com.googlesource.gerrit.plugins.replication</groupId>
- <artifactId>replication</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>com.googlesource.gerrit.plugins.reviewnotes</groupId>
- <artifactId>reviewnotes</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>com.googlesource.gerrit.plugins.validators</groupId>
- <artifactId>commit-message-length-validator</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
- </dependencies>
- </profile>
- </profiles>
-
- <build>
- <pluginManagement>
- <plugins>
- <plugin>
- <groupId>org.eclipse.m2e</groupId>
- <artifactId>lifecycle-mapping</artifactId>
- <version>1.0.0</version>
- <configuration>
- <lifecycleMappingMetadata>
- <pluginExecutions>
- <pluginExecution>
- <pluginExecutionFilter>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-dependency-plugin</artifactId>
- <versionRange>[2.0,)</versionRange>
- <goals>
- <goal>copy-dependencies</goal>
- <goal>unpack</goal>goal>
- </goals>
- </pluginExecutionFilter>
- <action>
- <execute />
- </action>
- </pluginExecution>
- </pluginExecutions>
- </lifecycleMappingMetadata>
- </configuration>
- </plugin>
- </plugins>
- </pluginManagement>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-war-plugin</artifactId>
- <configuration>
- <warName>gerrit-${project.version}</warName>
- <archiveClasses>true</archiveClasses>
- <attachClasses>true</attachClasses>
- <archive>
- <addMavenDescriptor>false</addMavenDescriptor>
- <manifestEntries>
- <Main-Class>Main</Main-Class>
- <Implementation-Title>Gerrit Code Review</Implementation-Title>
- <Implementation-Version>${project.version}</Implementation-Version>
- </manifestEntries>
- </archive>
- <overlays>
- <overlay>
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-main</artifactId>
- <type>jar</type>
- <includes>
- <include>Main.class</include>
- <include>com/google/gerrit/launcher/*.class</include>
- </includes>
- </overlay>
- </overlays>
- </configuration>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-dependency-plugin</artifactId>
- <executions>
- <execution>
- <id>copy-servlet-api</id>
- <configuration>
- <includeGroupIds>org.apache.tomcat,org.eclipse.jetty</includeGroupIds>
- <excludeArtifactIds>servlet-api</excludeArtifactIds>
- </configuration>
- <goals>
- <goal>copy-dependencies</goal>
- </goals>
- </execution>
- <execution>
- <id>copy-plugins</id>
- <configuration>
- <!-- CORE PLUGIN LIST -->
- <includeArtifactIds>commit-message-length-validator,replication,reviewnotes</includeArtifactIds>
- <includeTypes>jar</includeTypes>
- <stripVersion>true</stripVersion>
- <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/plugins</outputDirectory>
- </configuration>
- <goals>
- <goal>copy-dependencies</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-antrun-plugin</artifactId>
- <executions>
- <execution>
- <id>copy-servlet-api</id>
- <phase>process-classes</phase>
- <configuration>
- <target>
- <property name="src" location="${project.build.directory}/dependency" />
- <property name="dst" location="${project.build.directory}/${project.build.finalName}/WEB-INF/pgm-lib" />
-
- <mkdir dir="${dst}" />
- <copy overwrite="true" todir="${dst}">
- <fileset dir="${src}">
- <include name="*.jar" />
- </fileset>
- </copy>
- </target>
- </configuration>
- <goals>
- <goal>run</goal>
- </goals>
- </execution>
- <execution>
- <id>copy-license</id>
- <phase>process-classes</phase>
- <configuration>
- <target>
- <property name="src" location="${basedir}/../Documentation" />
- <property name="dst" location="${project.build.directory}/${project.build.finalName}" />
-
- <copy tofile="${dst}/LICENSES.txt"
- file="${src}/licenses.txt"
- overwrite="true" />
- </target>
- </configuration>
- <goals>
- <goal>run</goal>
- </goals>
- </execution>
- <execution>
- <id>include-documentation</id>
- <phase>process-classes</phase>
- <configuration>
- <target unless="gerrit.documentation.skip">
- <property name="src" location="${basedir}/../Documentation" />
- <property name="out" location="${project.build.directory}/${project.build.finalName}" />
- <property name="dst" location="${out}/Documentation" />
-
- <exec dir="${src}" executable="make">
- <arg value="VERSION=${project.version}" />
- <arg value="clean" />
- <arg value="all" />
- </exec>
-
- <mkdir dir="${dst}" />
- <copy overwrite="true" todir="${dst}">
- <fileset dir="${src}">
- <include name="*.html" />
- </fileset>
- </copy>
- </target>
- </configuration>
- <goals>
- <goal>run</goal>
- </goals>
- </execution>
- <execution>
- <id>include-release-notes</id>
- <phase>process-classes</phase>
- <configuration>
- <target unless="gerrit.documentation.skip">
- <property name="src" location="${basedir}/../ReleaseNotes" />
- <property name="out" location="${project.build.directory}/${project.build.finalName}" />
- <property name="dst" location="${out}/ReleaseNotes" />
-
- <exec dir="${src}" executable="make">
- <arg value="VERSION=${project.version}" />
- <arg value="clean" />
- <arg value="all" />
- </exec>
-
- <mkdir dir="${dst}" />
- <copy overwrite="true" todir="${dst}">
- <fileset dir="${src}">
- <include name="*.html" />
- </fileset>
- </copy>
- </target>
- </configuration>
- <goals>
- <goal>run</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 945547f..3c70cf6 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -18,12 +18,13 @@
import static com.google.inject.Stage.PRODUCTION;
import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.httpd.GerritUiOptions;
import com.google.gerrit.httpd.auth.openid.OpenIdModule;
import com.google.gerrit.httpd.plugins.HttpPluginModule;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.lucene.LuceneIndexModule;
import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.account.InternalAccountDirectory;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.AuthConfigModule;
@@ -37,17 +38,20 @@
import com.google.gerrit.server.git.LocalDiskRepositoryManager;
import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.index.IndexModule;
+import com.google.gerrit.server.index.NoIndexModule;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
import com.google.gerrit.server.mail.SmtpEmailSender;
import com.google.gerrit.server.patch.IntraLineWorkerPool;
import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
-import com.google.gerrit.server.plugins.PluginModule;
+import com.google.gerrit.server.plugins.PluginRestApiModule;
import com.google.gerrit.server.schema.DataSourceModule;
import com.google.gerrit.server.schema.DataSourceProvider;
import com.google.gerrit.server.schema.DataSourceType;
import com.google.gerrit.server.schema.DatabaseModule;
import com.google.gerrit.server.schema.SchemaModule;
import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gerrit.solr.SolrIndexModule;
import com.google.gerrit.sshd.SshKeyCacheImpl;
import com.google.gerrit.sshd.SshModule;
import com.google.gerrit.sshd.commands.MasterCommandModule;
@@ -249,10 +253,23 @@
modules.add(new ReceiveCommitsExecutorModule());
modules.add(new IntraLineWorkerPool.Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+ modules.add(new InternalAccountDirectory.Module());
modules.add(new DefaultCacheFactory.Module());
modules.add(new SmtpEmailSender.Module());
modules.add(new SignedTokenEmailTokenVerifier.Module());
- modules.add(new PluginModule());
+ modules.add(new PluginRestApiModule());
+ AbstractModule changeIndexModule;
+ switch (IndexModule.getIndexType(cfgInjector)) {
+ case LUCENE:
+ changeIndexModule = new LuceneIndexModule();
+ break;
+ case SOLR:
+ changeIndexModule = new SolrIndexModule();
+ break;
+ default:
+ changeIndexModule = new NoIndexModule();
+ }
+ modules.add(changeIndexModule);
modules.add(new CanonicalWebUrlModule() {
@Override
protected Class<? extends Provider<String>> provider() {
diff --git a/lib/BUCK b/lib/BUCK
new file mode 100644
index 0000000..d9f1ecf
--- /dev/null
+++ b/lib/BUCK
@@ -0,0 +1,261 @@
+include_defs('//lib/maven.defs')
+
+define_license(name = 'Apache1.1')
+define_license(name = 'Apache2.0')
+define_license(name = 'CC-BY3.0')
+define_license(name = 'MPL1.1')
+define_license(name = 'PublicDomain')
+define_license(name = 'antlr')
+define_license(name = 'args4j')
+define_license(name = 'automaton')
+define_license(name = 'bouncycastle')
+define_license(name = 'clippy')
+define_license(name = 'codemirror')
+define_license(name = 'diffy')
+define_license(name = 'h2')
+define_license(name = 'jgit')
+define_license(name = 'jsch')
+define_license(name = 'jsr305')
+define_license(name = 'ow2')
+define_license(name = 'postgresql')
+define_license(name = 'prologcafe')
+define_license(name = 'protobuf')
+define_license(name = 'slf4j')
+define_license(name = 'DO_NOT_DISTRIBUTE')
+
+maven_jar(
+ name = 'gwtorm',
+ id = 'gwtorm:gwtorm:1.7',
+ bin_sha1 = 'ee3b316a023f1422dd4b6654a3d51d0e5690809c',
+ src_sha1 = 'a145bde4cc87a4ff4cec283880e2a03be32cc868',
+ license = 'Apache2.0',
+ deps = [':protobuf'],
+ repository = GERRIT,
+)
+
+maven_jar(
+ name = 'gwtjsonrpc',
+ id = 'gwtjsonrpc:gwtjsonrpc:1.3',
+ bin_sha1 = '1717ba11ab0c5160798c80085220a63f864691d3',
+ src_sha1 = '9e01c5d7bd54f8e70066450b372a43c16404789e',
+ license = 'Apache2.0',
+ repository = GERRIT,
+)
+
+maven_jar(
+ name = 'gson',
+ id = 'com.google.code.gson:gson:2.1',
+ sha1 = '2e66da15851f9f5b5079228f856c2f090ba98c38',
+ license = 'Apache2.0',
+)
+
+maven_jar(
+ name = 'guava',
+ id = 'com.google.guava:guava:14.0.1',
+ sha1 = '69e12f4c6aeac392555f1ea86fab82b5e5e31ad4',
+ license = 'Apache2.0',
+)
+
+maven_jar(
+ name = 'asm3',
+ id = 'asm:asm:3.2',
+ sha1 = '9bc1511dec6adf302991ced13303e4140fdf9ab7',
+ license = 'ow2',
+ attach_source = False,
+)
+
+maven_jar(
+ name = 'ow2-asm',
+ id = 'org.ow2.asm:asm:4.0',
+ sha1 = '659add6efc75a4715d738e73f07505246edf4d66',
+ license = 'ow2',
+)
+
+maven_jar(
+ name = 'ow2-asm-analysis',
+ id = 'org.ow2.asm:asm-analysis:4.0',
+ sha1 = '1c45d52b6f6c638db13cf3ac12adeb56b254cdd7',
+ license = 'ow2',
+)
+
+maven_jar(
+ name = 'ow2-asm-tree',
+ id = 'org.ow2.asm:asm-tree:4.0',
+ sha1 = '67bd266cd17adcee486b76952ece4cc85fe248b8',
+ license = 'ow2',
+)
+
+maven_jar(
+ name = 'ow2-asm-util',
+ id = 'org.ow2.asm:asm-util:4.0',
+ sha1 = 'd7a65f54cda284f9706a750c23d64830bb740c39',
+ license = 'ow2',
+)
+
+maven_jar(
+ name = 'velocity',
+ id = 'org.apache.velocity:velocity:1.6.4',
+ sha1 = 'fcc58693dd8fc83d714fba149789be37cc19b66d',
+ license = 'Apache2.0',
+ deps = [
+ '//lib/commons:collections',
+ '//lib/commons:lang',
+ '//lib/commons:oro',
+ ],
+ exclude = ['META-INF/LICENSE', 'META-INF/NOTICE'],
+)
+
+maven_jar(
+ name = 'jsch',
+ id = 'com.jcraft:jsch:0.1.44-1',
+ sha1 = '2e9ae08de5a71bd0e0d3ba2558598181bfa71d4e',
+ license = 'jsch',
+)
+
+maven_jar(
+ name = 'servlet-api-3_0',
+ id = 'org.apache.tomcat:tomcat-servlet-api:7.0.32',
+ sha1 = 'e2f21e9868414122e6dd23ac66cf304d4290642c',
+ license = 'Apache2.0',
+ exclude = ['META-INF/NOTICE', 'META-INF/LICENSE'],
+)
+
+maven_jar(
+ name = 'jsr305',
+ id = 'com.google.code.findbugs:jsr305:1.3.9',
+ sha1 = '40719ea6961c0cb6afaeb6a921eaa1f6afd4cfdf',
+ license = 'jsr305',
+ attach_source = False,
+ exclude_java_sources = True,
+)
+
+maven_jar(
+ name = 'args4j',
+ id = 'args4j:args4j:2.0.16',
+ sha1 = '9f00fb12820743b9e05c686eba543d64dd43f2b1',
+ license = 'args4j',
+)
+
+maven_jar(
+ name = 'mime-util',
+ id = 'eu.medsea.mimeutil:mime-util:2.1.3',
+ sha1 = '0c9cfae15c74f62491d4f28def0dff1dabe52a47',
+ license = 'Apache2.0',
+ exclude = ['LICENSE.txt', 'README.txt'],
+ attach_source = False,
+)
+
+maven_jar(
+ name = 'juniversalchardet',
+ id = 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3',
+ sha1 = 'cd49678784c46aa8789c060538e0154013bb421b',
+ license = 'MPL1.1',
+)
+
+maven_jar(
+ name = 'automaton',
+ id = 'dk.brics.automaton:automaton:1.11-8',
+ sha1 = '6ebfa65eb431ff4b715a23be7a750cbc4cc96d0f',
+ license = 'automaton',
+)
+
+maven_jar(
+ name = 'pegdown',
+ id = 'org.pegdown:pegdown:1.1.0',
+ sha1 = '00bcc0c5b025b09ab85bb80a8311ce5c015d005b',
+ license = 'Apache2.0',
+ deps = [':parboiled-java'],
+ exclude = ['META-INF/LICENSE', 'META-INF/NOTICE'],
+)
+
+maven_jar(
+ name = 'parboiled-core',
+ id = 'org.parboiled:parboiled-core:1.1.3',
+ sha1 = '3fc3013adf98701efcc594a1ea99a3f841dc81bb',
+ license = 'Apache2.0',
+ attach_source = False,
+)
+
+maven_jar(
+ name = 'parboiled-java',
+ id = 'org.parboiled:parboiled-java:1.1.3',
+ sha1 = 'c2bf2935a8b3eca5f998557190cd6eb34f5536d0',
+ license = 'Apache2.0',
+ deps = [
+ ':parboiled-core',
+ ':ow2-asm-tree',
+ ':ow2-asm-analysis',
+ ':ow2-asm-util',
+ ],
+ attach_source = False,
+ visibility = [],
+)
+
+maven_jar(
+ name = 'h2',
+ id = 'com.h2database:h2:1.3.173',
+ sha1 = '3d9cc700d2c6b0b7a9bb59bd2b850e6518b6c209',
+ license = 'h2',
+)
+
+maven_jar(
+ name = 'postgresql',
+ id = 'postgresql:postgresql:9.1-901-1.jdbc4',
+ sha1 = '9bfabe48876ec38f6cbaa6931bad05c64a9ea942',
+ license = 'postgresql',
+ attach_source = False,
+)
+
+maven_jar(
+ name = 'protobuf',
+ # Must match version in gwtorm/pom.xml.
+ id = 'com.google.protobuf:protobuf-java:2.4.1',
+ bin_sha1 = '0c589509ec6fd86d5d2fda37e07c08538235d3b9',
+ src_sha1 = 'e406f69360f2a89cb4aa724ed996a1c5599af383',
+ license = 'protobuf',
+)
+
+maven_jar(
+ name = 'junit',
+ id = 'junit:junit:4.11',
+ sha1 = '4e031bb61df09069aeb2bffb4019e7a5034a4ee0',
+ license = 'DO_NOT_DISTRIBUTE',
+ deps = [':hamcrest-core'],
+)
+
+maven_jar(
+ name = 'hamcrest-core',
+ id = 'org.hamcrest:hamcrest-core:1.3',
+ sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0',
+ license = 'DO_NOT_DISTRIBUTE',
+ visibility = ['//lib:junit'],
+)
+
+maven_jar(
+ name = 'easymock',
+ id = 'org.easymock:easymock:3.1',
+ sha1 = '3e127311a86fc2e8f550ef8ee4abe094bbcf7e7e',
+ license = 'DO_NOT_DISTRIBUTE',
+ deps = [
+ ':cglib-2_2',
+ ':objenesis',
+ ],
+)
+
+maven_jar(
+ name = 'cglib-2_2',
+ id = 'cglib:cglib-nodep:2.2.2',
+ sha1 = '00d456bb230c70c0b95c76fb28e429d42f275941',
+ license = 'DO_NOT_DISTRIBUTE',
+ visibility = ['//lib:easymock'],
+ attach_source = False,
+)
+
+maven_jar(
+ name = 'objenesis',
+ id = 'org.objenesis:objenesis:1.2',
+ sha1 = 'bfcb0539a071a4c5a30690388903ac48c0667f2a',
+ license = 'DO_NOT_DISTRIBUTE',
+ visibility = ['//lib:easymock'],
+ attach_source = False,
+)
diff --git a/lib/LICENSE-Apache1.1 b/lib/LICENSE-Apache1.1
new file mode 100644
index 0000000..8eda4fc
--- /dev/null
+++ b/lib/LICENSE-Apache1.1
@@ -0,0 +1,51 @@
+The Apache Software License, Version 1.1
+
+Copyright (c) 2000-2002 The Apache Software Foundation. All rights
+reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+3. The end-user documentation included with the redistribution,
+ if any, must include the following acknowledgment:
+ "This product includes software developed by the
+ Apache Software Foundation (http://www.apache.org/)."
+ Alternately, this acknowledgment may appear in the software itself,
+ if and wherever such third-party acknowledgments normally appear.
+
+4. The names "Apache" and "Apache Software Foundation", "Jakarta-Oro"
+ must not be used to endorse or promote products derived from this
+ software without prior written permission. For written
+ permission, please contact apache@apache.org.
+
+5. Products derived from this software may not be called "Apache"
+ or "Jakarta-Oro", nor may "Apache" or "Jakarta-Oro" appear in their
+ name, without prior written permission of the Apache Software Foundation.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
+====================================================================
+
+This software consists of voluntary contributions made by many
+individuals on behalf of the Apache Software Foundation. For more
+information on the Apache Software Foundation, please see
+<http://www.apache.org/>.
diff --git a/lib/LICENSE-Apache2.0 b/lib/LICENSE-Apache2.0
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/lib/LICENSE-Apache2.0
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/lib/LICENSE-CC-BY3.0 b/lib/LICENSE-CC-BY3.0
new file mode 100644
index 0000000..39dbc91
--- /dev/null
+++ b/lib/LICENSE-CC-BY3.0
@@ -0,0 +1,333 @@
+link:http://creativecommons.org/licenses/by/3.0/us/[CC-BY 3.0]
+
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
+CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS
+PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE
+WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS
+PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND
+AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS
+LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU
+THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH
+TERMS AND CONDITIONS.
+
+1. Definitions
+
+ a. "Adaptation" means a work based upon the Work, or upon the Work
+ and other pre-existing works, such as a translation, adaptation,
+ derivative work, arrangement of music or other alterations of a
+ literary or artistic work, or phonogram or performance and
+ includes cinematographic adaptations or any other form in which
+ the Work may be recast, transformed, or adapted including in any
+ form recognizably derived from the original, except that a work
+ that constitutes a Collection will not be considered an
+ Adaptation for the purpose of this License. For the avoidance
+ of doubt, where the Work is a musical work, performance or
+ phonogram, the synchronization of the Work in timed-relation
+ with a moving image ("synching") will be considered an
+ Adaptation for the purpose of this License.
+
+ b. "Collection" means a collection of literary or artistic works,
+ such as encyclopedias and anthologies, or performances,
+ phonograms or broadcasts, or other works or subject matter other
+ than works listed in Section 1(f) below, which, by reason of the
+ selection and arrangement of their contents, constitute
+ intellectual creations, in which the Work is included in its
+ entirety in unmodified form along with one or more other
+ contributions, each constituting separate and independent works
+ in themselves, which together are assembled into a collective
+ whole. A work that constitutes a Collection will not be
+ considered an Adaptation (as defined above) for the purposes of
+ this License.
+
+ c. "Distribute" means to make available to the public the original
+ and copies of the Work or Adaptation, as appropriate, through
+ sale or other transfer of ownership.
+
+ d. "Licensor" means the individual, individuals, entity or entities
+ that offer(s) the Work under the terms of this License.
+
+ e. "Original Author" means, in the case of a literary or artistic
+ work, the individual, individuals, entity or entities who
+ created the Work or if no individual or entity can be
+ identified, the publisher; and in addition (i) in the case of a
+ performance the actors, singers, musicians, dancers, and other
+ persons who act, sing, deliver, declaim, play in, interpret or
+ otherwise perform literary or artistic works or expressions of
+ folklore; (ii) in the case of a phonogram the producer being the
+ person or legal entity who first fixes the sounds of a
+ performance or other sounds; and, (iii) in the case of
+ broadcasts, the organization that transmits the broadcast.
+
+ f. "Work" means the literary and/or artistic work offered under the
+ terms of this License including without limitation any
+ production in the literary, scientific and artistic domain,
+ whatever may be the mode or form of its expression including
+ digital form, such as a book, pamphlet and other writing; a
+ lecture, address, sermon or other work of the same nature; a
+ dramatic or dramatico-musical work; a choreographic work or
+ entertainment in dumb show; a musical composition with or
+ without words; a cinematographic work to which are assimilated
+ works expressed by a process analogous to cinematography; a work
+ of drawing, painting, architecture, sculpture, engraving or
+ lithography; a photographic work to which are assimilated works
+ expressed by a process analogous to photography; a work of
+ applied art; an illustration, map, plan, sketch or
+ three-dimensional work relative to geography, topography,
+ architecture or science; a performance; a broadcast; a
+ phonogram; a compilation of data to the extent it is protected
+ as a copyrightable work; or a work performed by a variety or
+ circus performer to the extent it is not otherwise considered a
+ literary or artistic work.
+
+ g. "You" means an individual or entity exercising rights under this
+ License who has not previously violated the terms of this
+ License with respect to the Work, or who has received express
+ permission from the Licensor to exercise rights under this
+ License despite a previous violation.
+
+ h. "Publicly Perform" means to perform public recitations of the
+ Work and to communicate to the public those public recitations,
+ by any means or process, including by wire or wireless means or
+ public digital performances; to make available to the public
+ Works in such a way that members of the public may access these
+ Works from a place and at a place individually chosen by them;
+ to perform the Work to the public by any means or process and
+ the communication to the public of the performances of the Work,
+ including by public digital performance; to broadcast and
+ rebroadcast the Work by any means including signs, sounds or
+ images.
+
+ i. "Reproduce" means to make copies of the Work by any means
+ including without limitation by sound or visual recordings and
+ the right of fixation and reproducing fixations of the Work,
+ including storage of a protected performance or phonogram in
+ digital form or other electronic medium.
+
+2. Fair Dealing Rights. Nothing in this License is intended to
+ reduce, limit, or restrict any uses free from copyright or rights
+ arising from limitations or exceptions that are provided for in
+ connection with the copyright protection under copyright law or
+ other applicable laws.
+
+3. License Grant. Subject to the terms and conditions of this
+ License, Licensor hereby grants You a worldwide, royalty-free,
+ non-exclusive, perpetual (for the duration of the applicable
+ copyright) license to exercise the rights in the Work as stated
+ below:
+
+ a. to Reproduce the Work, to incorporate the Work into one or more
+ Collections, and to Reproduce the Work as incorporated in the
+ Collections;
+
+ b. to create and Reproduce Adaptations provided that any such
+ Adaptation, including any translation in any medium, takes
+ reasonable steps to clearly label, demarcate or otherwise
+ identify that changes were made to the original Work. For
+ example, a translation could be marked "The original work was
+ translated from English to Spanish," or a modification could
+ indicate "The original work has been modified.";
+
+ c. to Distribute and Publicly Perform the Work including as
+ incorporated in Collections; and,
+
+ d. to Distribute and Publicly Perform Adaptations.
+
+ e. For the avoidance of doubt:
+
+ i. Non-waivable Compulsory License Schemes. In those
+ jurisdictions in which the right to collect royalties
+ through any statutory or compulsory licensing scheme
+ cannot be waived, the Licensor reserves the exclusive
+ right to collect such royalties for any exercise by You
+ of the rights granted under this License;
+
+ ii. Waivable Compulsory License Schemes. In those jurisdictions
+ in which the right to collect royalties through any
+ statutory or compulsory licensing scheme can be waived,
+ the Licensor waives the exclusive right to collect such
+ royalties for any exercise by You of the rights granted
+ under this License; and,
+
+ iii. Voluntary License Schemes. The Licensor waives the right to
+ collect royalties, whether individually or, in the event
+ that the Licensor is a member of a collecting society
+ that administers voluntary licensing schemes, via that
+ society, from any exercise by You of the rights granted
+ under this License.
+
+The above rights may be exercised in all media and formats whether now
+known or hereafter devised. The above rights include the right to
+make such modifications as are technically necessary to exercise the
+rights in other media and formats. Subject to Section 8(f), all
+rights not expressly granted by Licensor are hereby reserved.
+
+4. Restrictions. The license granted in Section 3 above is expressly
+ made subject to and limited by the following restrictions:
+
+ a. You may Distribute or Publicly Perform the Work only under the
+ terms of this License. You must include a copy of, or the
+ Uniform Resource Identifier (URI) for, this License with every
+ copy of the Work You Distribute or Publicly Perform. You may
+ not offer or impose any terms on the Work that restrict the
+ terms of this License or the ability of the recipient of the
+ Work to exercise the rights granted to that recipient under the
+ terms of the License. You may not sublicense the Work. You
+ must keep intact all notices that refer to this License and to
+ the disclaimer of warranties with every copy of the Work You
+ Distribute or Publicly Perform. When You Distribute or Publicly
+ Perform the Work, You may not impose any effective technological
+ measures on the Work that restrict the ability of a recipient of
+ the Work from You to exercise the rights granted to that
+ recipient under the terms of the License. This Section 4(a)
+ applies to the Work as incorporated in a Collection, but this
+ does not require the Collection apart from the Work itself to be
+ made subject to the terms of this License. If You create a
+ Collection, upon notice from any Licensor You must, to the
+ extent practicable, remove from the Collection any credit as
+ required by Section 4(b), as requested. If You create an
+ Adaptation, upon notice from any Licensor You must, to the
+ extent practicable, remove from the Adaptation any credit as
+ required by Section 4(b), as requested.
+
+ b. If You Distribute, or Publicly Perform the Work or any
+ Adaptations or Collections, You must, unless a request has been
+ made pursuant to Section 4(a), keep intact all copyright notices
+ for the Work and provide, reasonable to the medium or means You
+ are utilizing: (i) the name of the Original Author (or
+ pseudonym, if applicable) if supplied, and/or if the Original
+ Author and/or Licensor designate another party or parties (e.g.,
+ a sponsor institute, publishing entity, journal) for attribution
+ ("Attribution Parties") in Licensor's copyright notice, terms of
+ service or by other reasonable means, the name of such party or
+ parties; (ii) the title of the Work if supplied; (iii) to the
+ extent reasonably practicable, the URI, if any, that Licensor
+ specifies to be associated with the Work, unless such URI does
+ not refer to the copyright notice or licensing information for
+ the Work; and (iv) , consistent with Section 3(b), in the case
+ of an Adaptation, a credit identifying the use of the Work in
+ the Adaptation (e.g., "French translation of the Work by
+ Original Author," or "Screenplay based on original Work by
+ Original Author"). The credit required by this Section 4 (b)
+ may be implemented in any reasonable manner; provided, however,
+ that in the case of a Adaptation or Collection, at a minimum
+ such credit will appear, if a credit for all contributing
+ authors of the Adaptation or Collection appears, then as part of
+ these credits and in a manner at least as prominent as the
+ credits for the other contributing authors. For the avoidance
+ of doubt, You may only use the credit required by this Section
+ for the purpose of attribution in the manner set out above and,
+ by exercising Your rights under this License, You may not
+ implicitly or explicitly assert or imply any connection with,
+ sponsorship or endorsement by the Original Author, Licensor
+ and/or Attribution Parties, as appropriate, of You or Your use
+ of the Work, without the separate, express prior written
+ permission of the Original Author, Licensor and/or Attribution
+ Parties.
+
+ c. Except as otherwise agreed in writing by the Licensor or as may
+ be otherwise permitted by applicable law, if You Reproduce,
+ Distribute or Publicly Perform the Work either by itself or as
+ part of any Adaptations or Collections, You must not distort,
+ mutilate, modify or take other derogatory action in relation to
+ the Work which would be prejudicial to the Original Author's
+ honor or reputation. Licensor agrees that in those
+ jurisdictions (e.g. Japan), in which any exercise of the right
+ granted in Section 3(b) of this License (the right to make
+ Adaptations) would be deemed to be a distortion, mutilation,
+ modification or other derogatory action prejudicial to the
+ Original Author's honor and reputation, the Licensor will waive
+ or not assert, as appropriate, this Section, to the fullest
+ extent permitted by the applicable national law, to enable You
+ to reasonably exercise Your right under Section 3(b) of this
+ License (right to make Adaptations) but not otherwise.
+
+5. Representations, Warranties and Disclaimer
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING,
+LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR
+WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED,
+STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF
+TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE,
+NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY,
+OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE.
+SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES,
+SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY
+ APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY
+ LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE
+ OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE
+ WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+ DAMAGES.
+
+7. Termination
+
+ a. This License and the rights granted hereunder will terminate
+ automatically upon any breach by You of the terms of this
+ License. Individuals or entities who have received Adaptations
+ or Collections from You under this License, however, will not
+ have their licenses terminated provided such individuals or
+ entities remain in full compliance with those licenses.
+ Sections 1, 2, 5, 6, 7, and 8 will survive any termination of
+ this License.
+
+ b. Subject to the above terms and conditions, the license granted
+ here is perpetual (for the duration of the applicable copyright
+ in the Work). Notwithstanding the above, Licensor reserves the
+ right to release the Work under different license terms or to
+ stop distributing the Work at any time; provided, however that
+ any such election will not serve to withdraw this License (or
+ any other license that has been, or is required to be, granted
+ under the terms of this License), and this License will continue
+ in full force and effect unless terminated as stated above.
+
+8. Miscellaneous
+
+ a. Each time You Distribute or Publicly Perform the Work or a
+ Collection, the Licensor offers to the recipient a license to
+ the Work on the same terms and conditions as the license granted
+ to You under this License.
+
+ b. Each time You Distribute or Publicly Perform an Adaptation,
+ Licensor offers to the recipient a license to the original Work
+ on the same terms and conditions as the license granted to You
+ under this License.
+
+ c. If any provision of this License is invalid or unenforceable
+ under applicable law, it shall not affect the validity or
+ enforceability of the remainder of the terms of this License,
+ and without further action by the parties to this agreement,
+ such provision shall be reformed to the minimum extent necessary
+ to make such provision valid and enforceable.
+
+ d. No term or provision of this License shall be deemed waived and
+ no breach consented to unless such waiver or consent shall be in
+ writing and signed by the party to be charged with such waiver
+ or consent.
+
+ e. This License constitutes the entire agreement between the
+ parties with respect to the Work licensed here. There are no
+ understandings, agreements or representations with respect to
+ the Work not specified here. Licensor shall not be bound by any
+ additional provisions that may appear in any communication from
+ You. This License may not be modified without the mutual
+ written agreement of the Licensor and You.
+
+ f. The rights granted under, and the subject matter referenced, in
+ this License were drafted utilizing the terminology of the Berne
+ Convention for the Protection of Literary and Artistic Works (as
+ amended on September 28, 1979), the Rome Convention of 1961, the
+ WIPO Copyright Treaty of 1996, the WIPO Performances and
+ Phonograms Treaty of 1996 and the Universal Copyright Convention
+ (as revised on July 24, 1971). These rights and subject matter
+ take effect in the relevant jurisdiction in which the License
+ terms are sought to be enforced according to the corresponding
+ provisions of the implementation of those treaty provisions in
+ the applicable national law. If the standard suite of rights
+ granted under applicable copyright law includes additional
+ rights not granted under this License, such additional rights
+ are deemed to be included in the License; this License is not
+ intended to restrict the license of any rights under applicable
+ law.
diff --git a/lib/LICENSE-DO_NOT_DISTRIBUTE b/lib/LICENSE-DO_NOT_DISTRIBUTE
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/LICENSE-DO_NOT_DISTRIBUTE
diff --git a/lib/LICENSE-MPL1.1 b/lib/LICENSE-MPL1.1
new file mode 100644
index 0000000..06f9651
--- /dev/null
+++ b/lib/LICENSE-MPL1.1
@@ -0,0 +1,469 @@
+ MOZILLA PUBLIC LICENSE
+ Version 1.1
+
+ ---------------
+
+1. Definitions.
+
+ 1.0.1. "Commercial Use" means distribution or otherwise making the
+ Covered Code available to a third party.
+
+ 1.1. "Contributor" means each entity that creates or contributes to
+ the creation of Modifications.
+
+ 1.2. "Contributor Version" means the combination of the Original
+ Code, prior Modifications used by a Contributor, and the Modifications
+ made by that particular Contributor.
+
+ 1.3. "Covered Code" means the Original Code or Modifications or the
+ combination of the Original Code and Modifications, in each case
+ including portions thereof.
+
+ 1.4. "Electronic Distribution Mechanism" means a mechanism generally
+ accepted in the software development community for the electronic
+ transfer of data.
+
+ 1.5. "Executable" means Covered Code in any form other than Source
+ Code.
+
+ 1.6. "Initial Developer" means the individual or entity identified
+ as the Initial Developer in the Source Code notice required by Exhibit
+ A.
+
+ 1.7. "Larger Work" means a work which combines Covered Code or
+ portions thereof with code not governed by the terms of this License.
+
+ 1.8. "License" means this document.
+
+ 1.8.1. "Licensable" means having the right to grant, to the maximum
+ extent possible, whether at the time of the initial grant or
+ subsequently acquired, any and all of the rights conveyed herein.
+
+ 1.9. "Modifications" means any addition to or deletion from the
+ substance or structure of either the Original Code or any previous
+ Modifications. When Covered Code is released as a series of files, a
+ Modification is:
+ A. Any addition to or deletion from the contents of a file
+ containing Original Code or previous Modifications.
+
+ B. Any new file that contains any part of the Original Code or
+ previous Modifications.
+
+ 1.10. "Original Code" means Source Code of computer software code
+ which is described in the Source Code notice required by Exhibit A as
+ Original Code, and which, at the time of its release under this
+ License is not already Covered Code governed by this License.
+
+ 1.10.1. "Patent Claims" means any patent claim(s), now owned or
+ hereafter acquired, including without limitation, method, process,
+ and apparatus claims, in any patent Licensable by grantor.
+
+ 1.11. "Source Code" means the preferred form of the Covered Code for
+ making modifications to it, including all modules it contains, plus
+ any associated interface definition files, scripts used to control
+ compilation and installation of an Executable, or source code
+ differential comparisons against either the Original Code or another
+ well known, available Covered Code of the Contributor's choice. The
+ Source Code can be in a compressed or archival form, provided the
+ appropriate decompression or de-archiving software is widely available
+ for no charge.
+
+ 1.12. "You" (or "Your") means an individual or a legal entity
+ exercising rights under, and complying with all of the terms of, this
+ License or a future version of this License issued under Section 6.1.
+ For legal entities, "You" includes any entity which controls, is
+ controlled by, or is under common control with You. For purposes of
+ this definition, "control" means (a) the power, direct or indirect,
+ to cause the direction or management of such entity, whether by
+ contract or otherwise, or (b) ownership of more than fifty percent
+ (50%) of the outstanding shares or beneficial ownership of such
+ entity.
+
+2. Source Code License.
+
+ 2.1. The Initial Developer Grant.
+ The Initial Developer hereby grants You a world-wide, royalty-free,
+ non-exclusive license, subject to third party intellectual property
+ claims:
+ (a) under intellectual property rights (other than patent or
+ trademark) Licensable by Initial Developer to use, reproduce,
+ modify, display, perform, sublicense and distribute the Original
+ Code (or portions thereof) with or without Modifications, and/or
+ as part of a Larger Work; and
+
+ (b) under Patents Claims infringed by the making, using or
+ selling of Original Code, to make, have made, use, practice,
+ sell, and offer for sale, and/or otherwise dispose of the
+ Original Code (or portions thereof).
+
+ (c) the licenses granted in this Section 2.1(a) and (b) are
+ effective on the date Initial Developer first distributes
+ Original Code under the terms of this License.
+
+ (d) Notwithstanding Section 2.1(b) above, no patent license is
+ granted: 1) for code that You delete from the Original Code; 2)
+ separate from the Original Code; or 3) for infringements caused
+ by: i) the modification of the Original Code or ii) the
+ combination of the Original Code with other software or devices.
+
+ 2.2. Contributor Grant.
+ Subject to third party intellectual property claims, each Contributor
+ hereby grants You a world-wide, royalty-free, non-exclusive license
+
+ (a) under intellectual property rights (other than patent or
+ trademark) Licensable by Contributor, to use, reproduce, modify,
+ display, perform, sublicense and distribute the Modifications
+ created by such Contributor (or portions thereof) either on an
+ unmodified basis, with other Modifications, as Covered Code
+ and/or as part of a Larger Work; and
+
+ (b) under Patent Claims infringed by the making, using, or
+ selling of Modifications made by that Contributor either alone
+ and/or in combination with its Contributor Version (or portions
+ of such combination), to make, use, sell, offer for sale, have
+ made, and/or otherwise dispose of: 1) Modifications made by that
+ Contributor (or portions thereof); and 2) the combination of
+ Modifications made by that Contributor with its Contributor
+ Version (or portions of such combination).
+
+ (c) the licenses granted in Sections 2.2(a) and 2.2(b) are
+ effective on the date Contributor first makes Commercial Use of
+ the Covered Code.
+
+ (d) Notwithstanding Section 2.2(b) above, no patent license is
+ granted: 1) for any code that Contributor has deleted from the
+ Contributor Version; 2) separate from the Contributor Version;
+ 3) for infringements caused by: i) third party modifications of
+ Contributor Version or ii) the combination of Modifications made
+ by that Contributor with other software (except as part of the
+ Contributor Version) or other devices; or 4) under Patent Claims
+ infringed by Covered Code in the absence of Modifications made by
+ that Contributor.
+
+3. Distribution Obligations.
+
+ 3.1. Application of License.
+ The Modifications which You create or to which You contribute are
+ governed by the terms of this License, including without limitation
+ Section 2.2. The Source Code version of Covered Code may be
+ distributed only under the terms of this License or a future version
+ of this License released under Section 6.1, and You must include a
+ copy of this License with every copy of the Source Code You
+ distribute. You may not offer or impose any terms on any Source Code
+ version that alters or restricts the applicable version of this
+ License or the recipients' rights hereunder. However, You may include
+ an additional document offering the additional rights described in
+ Section 3.5.
+
+ 3.2. Availability of Source Code.
+ Any Modification which You create or to which You contribute must be
+ made available in Source Code form under the terms of this License
+ either on the same media as an Executable version or via an accepted
+ Electronic Distribution Mechanism to anyone to whom you made an
+ Executable version available; and if made available via Electronic
+ Distribution Mechanism, must remain available for at least twelve (12)
+ months after the date it initially became available, or at least six
+ (6) months after a subsequent version of that particular Modification
+ has been made available to such recipients. You are responsible for
+ ensuring that the Source Code version remains available even if the
+ Electronic Distribution Mechanism is maintained by a third party.
+
+ 3.3. Description of Modifications.
+ You must cause all Covered Code to which You contribute to contain a
+ file documenting the changes You made to create that Covered Code and
+ the date of any change. You must include a prominent statement that
+ the Modification is derived, directly or indirectly, from Original
+ Code provided by the Initial Developer and including the name of the
+ Initial Developer in (a) the Source Code, and (b) in any notice in an
+ Executable version or related documentation in which You describe the
+ origin or ownership of the Covered Code.
+
+ 3.4. Intellectual Property Matters
+ (a) Third Party Claims.
+ If Contributor has knowledge that a license under a third party's
+ intellectual property rights is required to exercise the rights
+ granted by such Contributor under Sections 2.1 or 2.2,
+ Contributor must include a text file with the Source Code
+ distribution titled "LEGAL" which describes the claim and the
+ party making the claim in sufficient detail that a recipient will
+ know whom to contact. If Contributor obtains such knowledge after
+ the Modification is made available as described in Section 3.2,
+ Contributor shall promptly modify the LEGAL file in all copies
+ Contributor makes available thereafter and shall take other steps
+ (such as notifying appropriate mailing lists or newsgroups)
+ reasonably calculated to inform those who received the Covered
+ Code that new knowledge has been obtained.
+
+ (b) Contributor APIs.
+ If Contributor's Modifications include an application programming
+ interface and Contributor has knowledge of patent licenses which
+ are reasonably necessary to implement that API, Contributor must
+ also include this information in the LEGAL file.
+
+ (c) Representations.
+ Contributor represents that, except as disclosed pursuant to
+ Section 3.4(a) above, Contributor believes that Contributor's
+ Modifications are Contributor's original creation(s) and/or
+ Contributor has sufficient rights to grant the rights conveyed by
+ this License.
+
+ 3.5. Required Notices.
+ You must duplicate the notice in Exhibit A in each file of the Source
+ Code. If it is not possible to put such notice in a particular Source
+ Code file due to its structure, then You must include such notice in a
+ location (such as a relevant directory) where a user would be likely
+ to look for such a notice. If You created one or more Modification(s)
+ You may add your name as a Contributor to the notice described in
+ Exhibit A. You must also duplicate this License in any documentation
+ for the Source Code where You describe recipients' rights or ownership
+ rights relating to Covered Code. You may choose to offer, and to
+ charge a fee for, warranty, support, indemnity or liability
+ obligations to one or more recipients of Covered Code. However, You
+ may do so only on Your own behalf, and not on behalf of the Initial
+ Developer or any Contributor. You must make it absolutely clear than
+ any such warranty, support, indemnity or liability obligation is
+ offered by You alone, and You hereby agree to indemnify the Initial
+ Developer and every Contributor for any liability incurred by the
+ Initial Developer or such Contributor as a result of warranty,
+ support, indemnity or liability terms You offer.
+
+ 3.6. Distribution of Executable Versions.
+ You may distribute Covered Code in Executable form only if the
+ requirements of Section 3.1-3.5 have been met for that Covered Code,
+ and if You include a notice stating that the Source Code version of
+ the Covered Code is available under the terms of this License,
+ including a description of how and where You have fulfilled the
+ obligations of Section 3.2. The notice must be conspicuously included
+ in any notice in an Executable version, related documentation or
+ collateral in which You describe recipients' rights relating to the
+ Covered Code. You may distribute the Executable version of Covered
+ Code or ownership rights under a license of Your choice, which may
+ contain terms different from this License, provided that You are in
+ compliance with the terms of this License and that the license for the
+ Executable version does not attempt to limit or alter the recipient's
+ rights in the Source Code version from the rights set forth in this
+ License. If You distribute the Executable version under a different
+ license You must make it absolutely clear that any terms which differ
+ from this License are offered by You alone, not by the Initial
+ Developer or any Contributor. You hereby agree to indemnify the
+ Initial Developer and every Contributor for any liability incurred by
+ the Initial Developer or such Contributor as a result of any such
+ terms You offer.
+
+ 3.7. Larger Works.
+ You may create a Larger Work by combining Covered Code with other code
+ not governed by the terms of this License and distribute the Larger
+ Work as a single product. In such a case, You must make sure the
+ requirements of this License are fulfilled for the Covered Code.
+
+4. Inability to Comply Due to Statute or Regulation.
+
+ If it is impossible for You to comply with any of the terms of this
+ License with respect to some or all of the Covered Code due to
+ statute, judicial order, or regulation then You must: (a) comply with
+ the terms of this License to the maximum extent possible; and (b)
+ describe the limitations and the code they affect. Such description
+ must be included in the LEGAL file described in Section 3.4 and must
+ be included with all distributions of the Source Code. Except to the
+ extent prohibited by statute or regulation, such description must be
+ sufficiently detailed for a recipient of ordinary skill to be able to
+ understand it.
+
+5. Application of this License.
+
+ This License applies to code to which the Initial Developer has
+ attached the notice in Exhibit A and to related Covered Code.
+
+6. Versions of the License.
+
+ 6.1. New Versions.
+ Netscape Communications Corporation ("Netscape") may publish revised
+ and/or new versions of the License from time to time. Each version
+ will be given a distinguishing version number.
+
+ 6.2. Effect of New Versions.
+ Once Covered Code has been published under a particular version of the
+ License, You may always continue to use it under the terms of that
+ version. You may also choose to use such Covered Code under the terms
+ of any subsequent version of the License published by Netscape. No one
+ other than Netscape has the right to modify the terms applicable to
+ Covered Code created under this License.
+
+ 6.3. Derivative Works.
+ If You create or use a modified version of this License (which you may
+ only do in order to apply it to code which is not already Covered Code
+ governed by this License), You must (a) rename Your license so that
+ the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape",
+ "MPL", "NPL" or any confusingly similar phrase do not appear in your
+ license (except to note that your license differs from this License)
+ and (b) otherwise make it clear that Your version of the license
+ contains terms which differ from the Mozilla Public License and
+ Netscape Public License. (Filling in the name of the Initial
+ Developer, Original Code or Contributor in the notice described in
+ Exhibit A shall not of themselves be deemed to be modifications of
+ this License.)
+
+7. DISCLAIMER OF WARRANTY.
+
+ COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+ WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF
+ DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING.
+ THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE
+ IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT,
+ YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE
+ COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER
+ OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+ ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+8. TERMINATION.
+
+ 8.1. This License and the rights granted hereunder will terminate
+ automatically if You fail to comply with terms herein and fail to cure
+ such breach within 30 days of becoming aware of the breach. All
+ sublicenses to the Covered Code which are properly granted shall
+ survive any termination of this License. Provisions which, by their
+ nature, must remain in effect beyond the termination of this License
+ shall survive.
+
+ 8.2. If You initiate litigation by asserting a patent infringement
+ claim (excluding declatory judgment actions) against Initial Developer
+ or a Contributor (the Initial Developer or Contributor against whom
+ You file such action is referred to as "Participant") alleging that:
+
+ (a) such Participant's Contributor Version directly or indirectly
+ infringes any patent, then any and all rights granted by such
+ Participant to You under Sections 2.1 and/or 2.2 of this License
+ shall, upon 60 days notice from Participant terminate prospectively,
+ unless if within 60 days after receipt of notice You either: (i)
+ agree in writing to pay Participant a mutually agreeable reasonable
+ royalty for Your past and future use of Modifications made by such
+ Participant, or (ii) withdraw Your litigation claim with respect to
+ the Contributor Version against such Participant. If within 60 days
+ of notice, a reasonable royalty and payment arrangement are not
+ mutually agreed upon in writing by the parties or the litigation claim
+ is not withdrawn, the rights granted by Participant to You under
+ Sections 2.1 and/or 2.2 automatically terminate at the expiration of
+ the 60 day notice period specified above.
+
+ (b) any software, hardware, or device, other than such Participant's
+ Contributor Version, directly or indirectly infringes any patent, then
+ any rights granted to You by such Participant under Sections 2.1(b)
+ and 2.2(b) are revoked effective as of the date You first made, used,
+ sold, distributed, or had made, Modifications made by that
+ Participant.
+
+ 8.3. If You assert a patent infringement claim against Participant
+ alleging that such Participant's Contributor Version directly or
+ indirectly infringes any patent where such claim is resolved (such as
+ by license or settlement) prior to the initiation of patent
+ infringement litigation, then the reasonable value of the licenses
+ granted by such Participant under Sections 2.1 or 2.2 shall be taken
+ into account in determining the amount or value of any payment or
+ license.
+
+ 8.4. In the event of termination under Sections 8.1 or 8.2 above,
+ all end user license agreements (excluding distributors and resellers)
+ which have been validly granted by You or any distributor hereunder
+ prior to termination shall survive termination.
+
+9. LIMITATION OF LIABILITY.
+
+ UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+ (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL
+ DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE,
+ OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR
+ ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY
+ CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL,
+ WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+ COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+ INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+ LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY
+ RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW
+ PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE
+ EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO
+ THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+10. U.S. GOVERNMENT END USERS.
+
+ The Covered Code is a "commercial item," as that term is defined in
+ 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+ software" and "commercial computer software documentation," as such
+ terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48
+ C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995),
+ all U.S. Government End Users acquire Covered Code with only those
+ rights set forth herein.
+
+11. MISCELLANEOUS.
+
+ This License represents the complete agreement concerning subject
+ matter hereof. If any provision of this License is held to be
+ unenforceable, such provision shall be reformed only to the extent
+ necessary to make it enforceable. This License shall be governed by
+ California law provisions (except to the extent applicable law, if
+ any, provides otherwise), excluding its conflict-of-law provisions.
+ With respect to disputes in which at least one party is a citizen of,
+ or an entity chartered or registered to do business in the United
+ States of America, any litigation relating to this License shall be
+ subject to the jurisdiction of the Federal Courts of the Northern
+ District of California, with venue lying in Santa Clara County,
+ California, with the losing party responsible for costs, including
+ without limitation, court costs and reasonable attorneys' fees and
+ expenses. The application of the United Nations Convention on
+ Contracts for the International Sale of Goods is expressly excluded.
+ Any law or regulation which provides that the language of a contract
+ shall be construed against the drafter shall not apply to this
+ License.
+
+12. RESPONSIBILITY FOR CLAIMS.
+
+ As between Initial Developer and the Contributors, each party is
+ responsible for claims and damages arising, directly or indirectly,
+ out of its utilization of rights under this License and You agree to
+ work with Initial Developer and Contributors to distribute such
+ responsibility on an equitable basis. Nothing herein is intended or
+ shall be deemed to constitute any admission of liability.
+
+13. MULTIPLE-LICENSED CODE.
+
+ Initial Developer may designate portions of the Covered Code as
+ "Multiple-Licensed". "Multiple-Licensed" means that the Initial
+ Developer permits you to utilize portions of the Covered Code under
+ Your choice of the NPL or the alternative licenses, if any, specified
+ by the Initial Developer in the file described in Exhibit A.
+
+EXHIBIT A -Mozilla Public License.
+
+ ``The contents of this file are subject to the Mozilla Public License
+ Version 1.1 (the "License"); you may not use this file except in
+ compliance with the License. You may obtain a copy of the License at
+ http://www.mozilla.org/MPL/
+
+ Software distributed under the License is distributed on an "AS IS"
+ basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+ License for the specific language governing rights and limitations
+ under the License.
+
+ The Original Code is ______________________________________.
+
+ The Initial Developer of the Original Code is ________________________.
+ Portions created by ______________________ are Copyright (C) ______
+ _______________________. All Rights Reserved.
+
+ Contributor(s): ______________________________________.
+
+ Alternatively, the contents of this file may be used under the terms
+ of the _____ license (the "[___] License"), in which case the
+ provisions of [______] License are applicable instead of those
+ above. If you wish to allow use of your version of this file only
+ under the terms of the [____] License and not to allow others to use
+ your version of this file under the MPL, indicate your decision by
+ deleting the provisions above and replace them with the notice and
+ other provisions required by the [___] License. If you do not delete
+ the provisions above, a recipient may use your version of this file
+ under either the MPL or the [___] License."
+
+ [NOTE: The text of this Exhibit A may differ slightly from the text of
+ the notices in the Source Code files of the Original Code. You should
+ use the text of this Exhibit A rather than the text found in the
+ Original Code Source Code for Your Modifications.]
diff --git a/lib/LICENSE-PublicDomain b/lib/LICENSE-PublicDomain
new file mode 100644
index 0000000..8a71ce0
--- /dev/null
+++ b/lib/LICENSE-PublicDomain
@@ -0,0 +1 @@
+This software has been placed in the public domain by its author(s).
diff --git a/lib/LICENSE-antlr b/lib/LICENSE-antlr
new file mode 100644
index 0000000..6041290
--- /dev/null
+++ b/lib/LICENSE-antlr
@@ -0,0 +1,29 @@
+Copyright (c) 2003-2008, Terence Parr
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of the author nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-args4j b/lib/LICENSE-args4j
new file mode 100644
index 0000000..36cd75f
--- /dev/null
+++ b/lib/LICENSE-args4j
@@ -0,0 +1,32 @@
+Copyright (c) 2003, Kohsuke Kawaguchi
+All rights reserved.
+
+Redistribution and use in source and binary forms,
+with or without modification, are permitted provided
+that the following conditions are met:
+
+ * Redistributions of source code must retain
+ the above copyright notice, this list of
+ conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce
+ the above copyright notice, this list of
+ conditions and the following disclaimer in
+ the documentation and/or other materials
+ provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT
+HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-automaton b/lib/LICENSE-automaton
new file mode 100644
index 0000000..72dcb1c
--- /dev/null
+++ b/lib/LICENSE-automaton
@@ -0,0 +1,28 @@
+Copyright (c) 2007-2009, dk.brics.automaton
+All rights reserved.
+
+http://www.opensource.org/licenses/bsd-license.php
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of the JSR305 expert group nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-bouncycastle b/lib/LICENSE-bouncycastle
new file mode 100644
index 0000000..d17a4bc
--- /dev/null
+++ b/lib/LICENSE-bouncycastle
@@ -0,0 +1,21 @@
+Copyright (c) 2000 - 2012 The Legion Of The Bouncy Castle
+(http://www.bouncycastle.org)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/LICENSE-clippy b/lib/LICENSE-clippy
new file mode 100644
index 0000000..b0feeae
--- /dev/null
+++ b/lib/LICENSE-clippy
@@ -0,0 +1,20 @@
+Copyright (c) 2008 Tom Preston-Werner
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/LICENSE-codemirror b/lib/LICENSE-codemirror
new file mode 100644
index 0000000..7df9fec
--- /dev/null
+++ b/lib/LICENSE-codemirror
@@ -0,0 +1,44 @@
+Copyright (C) 2013 by Marijn Haverbeke <marijnh@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+----
+
+codemirror Python mode:
+----
+The MIT License
+
+Copyright (c) 2010 Timothy Farrell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/lib/LICENSE-diffy b/lib/LICENSE-diffy
new file mode 100644
index 0000000..2b58536
--- /dev/null
+++ b/lib/LICENSE-diffy
@@ -0,0 +1 @@
+link:http://creativecommons.org/licenses/by/3.0/us/[CC-BY 3.0] (c) Sara Owens, inkylabs.com
diff --git a/lib/LICENSE-h2 b/lib/LICENSE-h2
new file mode 100644
index 0000000..1be4fba8
--- /dev/null
+++ b/lib/LICENSE-h2
@@ -0,0 +1,710 @@
+H2 is dual licensed and available under a modified version of the
+MPL 1.1 (Mozilla Public License) or under the (unmodified) EPL 1.0.
+----
+
+link:http://www.h2database.com/html/license.html[H2 License]
+
+----
+H2 License - Version 1.0
+1. Definitions
+
+1.0.1. "Commercial Use" means distribution or otherwise making the
+ Covered Code available to a third party.
+
+1.1. "Contributor" means each entity that creates or contributes
+ to the creation of Modifications.
+
+1.2. "Contributor Version" means the combination of the Original
+ Code, prior Modifications used by a Contributor, and the
+ Modifications made by that particular Contributor.
+
+1.3. "Covered Code" means the Original Code or Modifications or
+ the combination of the Original Code and Modifications, in each
+ case including portions thereof.
+
+1.4. "Electronic Distribution Mechanism" means a mechanism generally
+ accepted in the software development community for the electronic
+ transfer of data.
+
+1.5. "Executable" means Covered Code in any form other than Source Code.
+
+1.6. "Initial Developer" means the individual or entity identified
+ as the Initial Developer in the Source Code notice required
+ by Exhibit A.
+
+1.7. "Larger Work" means a work which combines Covered Code or
+ portions thereof with code not governed by the terms of this
+ License.
+
+1.8. "License" means this document.
+
+1.8.1. "Licensable" means having the right to grant, to the maximum
+ extent possible, whether at the time of the initial grant
+ or subsequently acquired, any and all of the rights conveyed
+ herein.
+
+1.9. "Modifications" means any addition to or deletion from the
+ substance or structure of either the Original Code or any
+ previous Modifications. When Covered Code is released as a
+ series of files, a Modification is:
+
+1.9.a. Any addition to or deletion from the contents of a file
+ containing Original Code or previous Modifications.
+
+1.9.b. Any new file that contains any part of the Original Code or
+ previous Modifications.
+
+1.10. "Original Code" means Source Code of computer software
+ code which is described in the Source Code notice required
+ by Exhibit A as Original Code, and which, at the time of
+ its release under this License is not already Covered Code
+ governed by this License.
+
+1.10.1. "Patent Claims" means any patent claim(s), now owned or
+ hereafter acquired, including without limitation, method,
+ process, and apparatus claims, in any patent Licensable
+ by grantor.
+
+1.11. "Source Code" means the preferred form of the Covered Code
+ for making modifications to it, including all modules it
+ contains, plus any associated interface definition files,
+ scripts used to control compilation and installation of an
+ Executable, or source code differential comparisons against
+ either the Original Code or another well known, available
+ Covered Code of the Contributor's choice. The Source Code can
+ be in a compressed or archival form, provided the appropriate
+ decompression or de-archiving software is widely available
+ for no charge.
+
+1.12. "You" (or "Your") means an individual or a legal entity
+ exercising rights under, and complying with all of the terms
+ of, this License or a future version of this License issued
+ under Section 6.1. For legal entities, "You" includes any
+ entity which controls, is controlled by, or is under common
+ control with You. For purposes of this definition, "control"
+ means (a) the power, direct or indirect, to cause the direction
+ or management of such entity, whether by contract or otherwise,
+ or (b) ownership of more than fifty percent (50%) of the
+ outstanding shares or beneficial ownership of such entity.
+
+2. Source Code License
+
+2.1. The Initial Developer Grant
+
+The Initial Developer hereby grants You a world-wide, royalty-free,
+non-exclusive license, subject to third party intellectual property
+claims:
+
+2.1.a. under intellectual property rights (other than patent
+ or trademark) Licensable by Initial Developer to use,
+ reproduce, modify, display, perform, sublicense and distribute
+ the Original Code (or portions thereof) with or without
+ Modifications, and/or as part of a Larger Work; and
+
+2.1.b. under Patents Claims infringed by the making, using or selling
+ of Original Code, to make, have made, use, practice, sell,
+ and offer for sale, and/or otherwise dispose of the Original
+ Code (or portions thereof).
+
+2.1.c. the licenses granted in this Section 2.1 (a) and (b) are
+ effective on the date Initial Developer first distributes
+ Original Code under the terms of this License.
+
+2.1.d. Notwithstanding Section 2.1 (b) above, no patent license is
+ granted: 1) for code that You delete from the Original Code;
+ 2) separate from the Original Code; or 3) for infringements
+ caused by: i) the modification of the Original Code or ii)
+ the combination of the Original Code with other software
+ or devices.
+
+2.2. Contributor Grant
+
+Subject to third party intellectual property claims, each Contributor
+hereby grants You a world-wide, royalty-free, non-exclusive license
+
+2.2.a. under intellectual property rights (other than patent or
+ trademark) Licensable by Contributor, to use, reproduce,
+ modify, display, perform, sublicense and distribute the
+ Modifications created by such Contributor (or portions
+ thereof) either on an unmodified basis, with other
+ Modifications, as Covered Code and/or as part of a Larger
+ Work; and
+
+2.2.b. under Patent Claims infringed by the making, using, or selling
+ of Modifications made by that Contributor either alone and/or
+ in combination with its Contributor Version (or portions
+ of such combination), to make, use, sell, offer for sale,
+ have made, and/or otherwise dispose of: 1) Modifications
+ made by that Contributor (or portions thereof); and 2) the
+ combination of Modifications made by that Contributor with
+ its Contributor Version (or portions of such combination).
+
+2.2.c. the licenses granted in Sections 2.2 (a) and 2.2 (b) are
+ effective on the date Contributor first makes Commercial
+ Use of the Covered Code.
+
+2.2.c. Notwithstanding Section 2.2 (b) above, no patent license is
+ granted: 1) for any code that Contributor has deleted from
+ the Contributor Version; 2) separate from the Contributor
+ Version; 3) for infringements caused by: i) third party
+ modifications of Contributor Version or ii) the combination
+ of Modifications made by that Contributor with other software
+ (except as part of the Contributor Version) or other devices;
+ or 4) under Patent Claims infringed by Covered Code in the
+ absence of Modifications made by that Contributor.
+
+3. Distribution Obligations
+
+3.1. Application of License
+
+The Modifications which You create or to which You contribute
+are governed by the terms of this License, including without
+limitation Section 2.2. The Source Code version of Covered Code may
+be distributed only under the terms of this License or a future
+version of this License released under Section 6.1, and You must
+include a copy of this License with every copy of the Source Code
+You distribute. You may not offer or impose any terms on any Source
+Code version that alters or restricts the applicable version of
+this License or the recipients' rights hereunder. However, You
+may include an additional document offering the additional rights
+described in Section 3.5.
+
+3.2. Availability of Source Code
+
+Any Modification which You create or to which You contribute must
+be made available in Source Code form under the terms of this
+License either on the same media as an Executable version or via
+an accepted Electronic Distribution Mechanism to anyone to whom
+you made an Executable version available; and if made available
+via Electronic Distribution Mechanism, must remain available for
+at least twelve (12) months after the date it initially became
+available, or at least six (6) months after a subsequent version
+of that particular Modification has been made available to such
+recipients. You are responsible for ensuring that the Source Code
+version remains available even if the Electronic Distribution
+Mechanism is maintained by a third party.
+
+3.3. Description of Modifications
+
+You must cause all Covered Code to which You contribute to contain
+a file documenting the changes You made to create that Covered
+Code and the date of any change. You must include a prominent
+statement that the Modification is derived, directly or indirectly,
+from Original Code provided by the Initial Developer and including
+the name of the Initial Developer in (a) the Source Code, and (b)
+in any notice in an Executable version or related documentation in
+which You describe the origin or ownership of the Covered Code.
+
+3.4. Intellectual Property Matters
+
+3.4.a. Third Party Claims: If Contributor has knowledge that
+ a license under a third party's intellectual property
+ rights is required to exercise the rights granted by such
+ Contributor under Sections 2.1 or 2.2, Contributor must
+ include a text file with the Source Code distribution titled
+ "LEGAL" which describes the claim and the party making the
+ claim in sufficient detail that a recipient will know whom
+ to contact. If Contributor obtains such knowledge after the
+ Modification is made available as described in Section 3.2,
+ Contributor shall promptly modify the LEGAL file in all
+ copies Contributor makes available thereafter and shall take
+ other steps (such as notifying appropriate mailing lists or
+ newsgroups) reasonably calculated to inform those who received
+ the Covered Code that new knowledge has been obtained.
+
+3.4.b. Contributor APIs: If Contributor's Modifications include
+ an application programming interface and Contributor has
+ knowledge of patent licenses which are reasonably necessary
+ to implement that API, Contributor must also include this
+ information in the legal file.
+
+3.4.c. Representations: Contributor represents that, except as
+ disclosed pursuant to Section 3.4 (a) above, Contributor
+ believes that Contributor's Modifications are Contributor's
+ original creation(s) and/or Contributor has sufficient rights
+ to grant the rights conveyed by this License.
+
+3.5. Required Notices
+
+You must duplicate the notice in Exhibit A in each file of
+the Source Code. If it is not possible to put such notice in a
+particular Source Code file due to its structure, then You must
+include such notice in a location (such as a relevant directory)
+where a user would be likely to look for such a notice. If You
+created one or more Modification(s) You may add your name as a
+Contributor to the notice described in Exhibit A. You must also
+duplicate this License in any documentation for the Source Code
+where You describe recipients' rights or ownership rights relating
+to Covered Code. You may choose to offer, and to charge a fee for,
+warranty, support, indemnity or liability obligations to one or
+more recipients of Covered Code. However, You may do so only on
+Your own behalf, and not on behalf of the Initial Developer or
+any Contributor. You must make it absolutely clear than any such
+warranty, support, indemnity or liability obligation is offered by
+You alone, and You hereby agree to indemnify the Initial Developer
+and every Contributor for any liability incurred by the Initial
+Developer or such Contributor as a result of warranty, support,
+indemnity or liability terms You offer.
+
+3.6. Distribution of Executable Versions
+
+You may distribute Covered Code in Executable form only if the
+requirements of Sections 3.1, 3.2, 3.3, 3.4 and 3.5 have been met
+for that Covered Code, and if You include a notice stating that
+the Source Code version of the Covered Code is available under the
+terms of this License, including a description of how and where
+You have fulfilled the obligations of Section 3.2. The notice
+must be conspicuously included in any notice in an Executable
+version, related documentation or collateral in which You describe
+recipients' rights relating to the Covered Code. You may distribute
+the Executable version of Covered Code or ownership rights under
+a license of Your choice, which may contain terms different from
+this License, provided that You are in compliance with the terms
+of this License and that the license for the Executable version
+does not attempt to limit or alter the recipient's rights in the
+Source Code version from the rights set forth in this License. If
+You distribute the Executable version under a different license You
+must make it absolutely clear that any terms which differ from this
+License are offered by You alone, not by the Initial Developer or any
+Contributor. You hereby agree to indemnify the Initial Developer and
+every Contributor for any liability incurred by the Initial Developer
+or such Contributor as a result of any such terms You offer.
+
+3.7. Larger Works
+
+You may create a Larger Work by combining Covered Code with other
+code not governed by the terms of this License and distribute the
+Larger Work as a single product. In such a case, You must make sure
+the requirements of this License are fulfilled for the Covered Code.
+
+4. Inability to Comply Due to Statute or Regulation.
+
+If it is impossible for You to comply with any of the terms of
+this License with respect to some or all of the Covered Code due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description
+must be included in the legal file described in Section 3.4 and
+must be included with all distributions of the Source Code. Except
+to the extent prohibited by statute or regulation, such description
+must be sufficiently detailed for a recipient of ordinary skill to
+be able to understand it.
+
+5. Application of this License.
+
+This License applies to code to which the Initial Developer has
+attached the notice in Exhibit A and to related Covered Code.
+
+6. Versions of the License.
+
+6.1. New Versions
+
+The H2 Group may publish revised and/or new versions of the License
+from time to time. Each version will be given a distinguishing
+version number.
+
+6.2. Effect of New Versions
+
+Once Covered Code has been published under a particular version of
+the License, You may always continue to use it under the terms of
+that version. You may also choose to use such Covered Code under the
+terms of any subsequent version of the License published by the H2
+Group. No one other than the H2 Group has the right to modify the
+terms applicable to Covered Code created under this License.
+
+6.3. Derivative Works
+
+If You create or use a modified version of this License (which you
+may only do in order to apply it to code which is not already Covered
+Code governed by this License), You must (a) rename Your license so
+that the phrases "H2 Group", "H2" or any confusingly similar phrase
+do not appear in your license (except to note that your license
+differs from this License) and (b) otherwise make it clear that
+Your version of the license contains terms which differ from the
+H2 License. (Filling in the name of the Initial Developer, Original
+Code or Contributor in the notice described in Exhibit A shall not
+of themselves be deemed to be modifications of this License.)
+
+7. Disclaimer of Warranty
+
+Covered code is provided under this license on an "as is" basis,
+without warranty of any kind, either expressed or implied,
+including, without limitation, warranties that the covered code
+is free of defects, merchantable, fit for a particular purpose or
+non-infringing. The entire risk as to the quality and performance
+of the covered code is with you. Should any covered code prove
+defective in any respect, you (not the initial developer or any
+other contributor) assume the cost of any necessary servicing,
+repair or correction. This disclaimer of warranty constitutes
+an essential part of this license. No use of any covered code is
+authorized hereunder except under this disclaimer.
+
+8. Termination
+
+8.1. This License and the rights granted hereunder will terminate
+ automatically if You fail to comply with terms herein and
+ fail to cure such breach within 30 days of becoming aware
+ of the breach. All sublicenses to the Covered Code which
+ are properly granted shall survive any termination of this
+ License. Provisions which, by their nature, must remain in
+ effect beyond the termination of this License shall survive.
+
+8.2. If You initiate litigation by asserting a patent infringement
+ claim (excluding declaratory judgment actions) against
+ Initial Developer or a Contributor (the Initial Developer or
+ Contributor against whom You file such action is referred to as
+ "Participant") alleging that:
+
+8.2.a. such Participant's Contributor Version directly or indirectly
+ infringes any patent, then any and all rights granted by
+ such Participant to You under Sections 2.1 and/or 2.2 of this
+ License shall, upon 60 days notice from Participant terminate
+ prospectively, unless if within 60 days after receipt of
+ notice You either: (i) agree in writing to pay Participant
+ a mutually agreeable reasonable royalty for Your past and
+ future use of Modifications made by such Participant, or (ii)
+ withdraw Your litigation claim with respect to the Contributor
+ Version against such Participant. If within 60 days of notice,
+ a reasonable royalty and payment arrangement are not mutually
+ agreed upon in writing by the parties or the litigation claim
+ is not withdrawn, the rights granted by Participant to You
+ under Sections 2.1 and/or 2.2 automatically terminate at
+ the expiration of the 60 day notice period specified above.
+
+8.2.b. any software, hardware, or device, other than such
+ Participant's Contributor Version, directly or indirectly
+ infringes any patent, then any rights granted to You by
+ such Participant under Sections 2.1(b) and 2.2(b) are
+ revoked effective as of the date You first made, used,
+ sold, distributed, or had made, Modifications made by that
+ Participant.
+
+8.3. If You assert a patent infringement claim against Participant
+ alleging that such Participant's Contributor Version directly
+ or indirectly infringes any patent where such claim is resolved
+ (such as by license or settlement) prior to the initiation of
+ patent infringement litigation, then the reasonable value of
+ the licenses granted by such Participant under Sections 2.1
+ or 2.2 shall be taken into account in determining the amount
+ or value of any payment or license.
+
+8.4. In the event of termination under Sections 8.1 or 8.2 above,
+ all end user license agreements (excluding distributors and
+ resellers) which have been validly granted by You or any
+ distributor hereunder prior to termination shall survive
+ termination.
+
+9. Limitation of Liability
+
+Under no circumstances and under no legal theory, whether tort
+(including negligence), contract, or otherwise, shall you, the
+initial developer, any other contributor, or any distributor of
+covered code, or any supplier of any of such parties, be liable to
+any person for any indirect, special, incidental, or consequential
+damages of any character including, without limitation, damages for
+loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses, even if such party
+shall have been informed of the possibility of such damages. This
+limitation of liability shall not apply to liability for death or
+personal injury resulting from such party's negligence to the extent
+applicable law prohibits such limitation. Some jurisdictions do not
+allow the exclusion or limitation of incidental or consequential
+damages, so this exclusion and limitation may not apply to you.
+
+10. United States Government End Users
+
+The Covered Code is a "commercial item", as that term is defined in
+48 C.F.R. 2.101 (October 1995), consisting of "commercial computer
+software" and "commercial computer software documentation", as such
+terms are used in 48 C.F.R. 12.212 (September 1995). Consistent
+with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4
+(June 1995), all U.S. Government End Users acquire Covered Code
+with only those rights set forth herein.
+
+11. Miscellaneous
+
+This License represents the complete agreement concerning subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. This License shall be governed
+by California law provisions (except to the extent applicable
+law, if any, provides otherwise), excluding its conflict-of-law
+provisions. With respect to disputes in which at least one party is
+a citizen of, or an entity chartered or registered to do business in
+United States of America, any litigation relating to this License
+shall be subject to the jurisdiction of the Federal Courts of the
+Northern District of California, with venue lying in Santa Clara
+County, California, with the losing party responsible for costs,
+including without limitation, court costs and reasonable attorneys'
+fees and expenses. The application of the United Nations Convention
+on Contracts for the International Sale of Goods is expressly
+excluded. Any law or regulation which provides that the language of
+a contract shall be construed against the drafter shall not apply
+to this License.
+
+12. Responsibility for Claims
+
+As between Initial Developer and the Contributors, each party is
+responsible for claims and damages arising, directly or indirectly,
+out of its utilization of rights under this License and You agree
+to work with Initial Developer and Contributors to distribute such
+responsibility on an equitable basis. Nothing herein is intended
+or shall be deemed to constitute any admission of liability.
+
+13. Multiple-Licensed Code
+
+Initial Developer may designate portions of the Covered Code as
+"Multiple-Licensed". "Multiple-Licensed" means that the Initial
+Developer permits you to utilize portions of the Covered Code under
+Your choice of this or the alternative licenses, if any, specified
+by the Initial Developer in the file described in Exhibit A.
+
+Exhibit A
+
+Multiple-Licensed under the H2 License, Version 1.0,
+and under the Eclipse Public License, Version 1.0
+(http://h2database.com/html/license.html).
+Initial Developer: H2 Group
+----
+
+----
+Eclipse Public License - v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
+OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+ documentation distributed under this Agreement, and
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from
+and are distributed by that particular Contributor. A Contribution
+'originates' from a Contributor if it was added to the Program
+by such Contributor itself or anyone acting on such Contributor's
+behalf. Contributions do not include additions to the Program which:
+(i) are separate modules of software distributed in conjunction
+with the Program under their own license agreement, and (ii) are
+not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor
+which are necessarily infringed by the use or sale of its
+Contribution alone or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with
+this Agreement.
+
+"Recipient" means anyone who receives the Program under this
+Agreement, including all Contributors.
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby
+ grants Recipient a non-exclusive, worldwide, royalty-free copyright
+ license to reproduce, prepare derivative works of, publicly display,
+ publicly perform, distribute and sublicense the Contribution of such
+ Contributor, if any, and such derivative works, in source code and
+ object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby
+ grants Recipient a non-exclusive, worldwide, royalty-free patent
+ license under Licensed Patents to make, use, sell, offer to sell,
+ import and otherwise transfer the Contribution of such Contributor,
+ if any, in source code and object code form. This patent license
+ shall apply to the combination of the Contribution and the Program
+ if, at the time the Contribution is added by the Contributor, such
+ addition of the Contribution causes such combination to be covered
+ by the Licensed Patents. The patent license shall not apply to any
+ other combinations which include the Contribution. No hardware per
+ se is licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the
+ licenses to its Contributions set forth herein, no assurances are
+ provided by any Contributor that the Program does not infringe
+ the patent or other intellectual property rights of any other
+ entity. Each Contributor disclaims any liability to Recipient
+ for claims brought by any other entity based on infringement
+ of intellectual property rights or otherwise. As a condition to
+ exercising the rights and licenses granted hereunder, each Recipient
+ hereby assumes sole responsibility to secure any other intellectual
+ property rights needed, if any. For example, if a third party patent
+ license is required to allow Recipient to distribute the Program,
+ it is Recipient's responsibility to acquire that license before
+ distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has
+ sufficient copyright rights in its Contribution, if any, to grant
+ the copyright license set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code
+ form under its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties
+ and conditions, express and implied, including warranties or
+ conditions of title and non-infringement, and implied warranties or
+ conditions of merchantability and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability
+ for damages, including direct, indirect, special, incidental and
+ consequential damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement
+ are offered by that Contributor alone and not by any other
+ party; and
+
+iv) states that source code for the Program is available from such
+ Contributor, and informs licensees how to obtain it in a reasonable
+ manner on or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained
+within the Program.
+
+Each Contributor must identify itself as the originator of its
+Contribution, if any, in a manner that reasonably allows subsequent
+Recipients to identify the originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain
+responsibilities with respect to end users, business partners and the
+like. While this license is intended to facilitate the commercial
+use of the Program, the Contributor who includes the Program in a
+commercial product offering should do so in a manner which does not
+create potential liability for other Contributors. Therefore, if a
+Contributor includes the Program in a commercial product offering,
+such Contributor ("Commercial Contributor") hereby agrees to defend
+and indemnify every other Contributor ("Indemnified Contributor")
+against any losses, damages and costs (collectively "Losses") arising
+from claims, lawsuits and other legal actions brought by a third
+party against the Indemnified Contributor to the extent caused by
+the acts or omissions of such Commercial Contributor in connection
+with its distribution of the Program in a commercial product
+offering. The obligations in this section do not apply to any claims
+or Losses relating to any actual or alleged intellectual property
+infringement. In order to qualify, an Indemnified Contributor must:
+a) promptly notify the Commercial Contributor in writing of such
+claim, and b) allow the Commercial Contributor to control, and
+cooperate with the Commercial Contributor in, the defense and any
+related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a
+commercial product offering, Product X. That Contributor is then a
+Commercial Contributor. If that Commercial Contributor then makes
+performance claims, or offers warranties related to Product X, those
+performance claims and warranties are such Commercial Contributor's
+responsibility alone. Under this section, the Commercial Contributor
+would have to defend claims against the other Contributors related
+to those performance claims and warranties, and if a court requires
+any other Contributor to pay any damages as a result, the Commercial
+Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
+PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
+WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
+OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
+responsible for determining the appropriateness of using and
+distributing the Program and assumes all risks associated with
+its exercise of rights under this Agreement , including but not
+limited to the risks and costs of program errors, compliance with
+applicable laws, damage to or loss of data, programs or equipment,
+and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
+NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY
+RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further
+action by the parties hereto, such provision shall be reformed
+to the minimum extent necessary to make such provision valid and
+enforceable.
+
+If Recipient institutes patent litigation against any entity
+(including a cross-claim or counterclaim in a lawsuit) alleging
+that the Program itself (excluding combinations of the Program with
+other software or hardware) infringes such Recipient's patent(s),
+then such Recipient's rights granted under Section 2(b) shall
+terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if
+it fails to comply with any of the material terms or conditions
+of this Agreement and does not cure such failure in a reasonable
+period of time after becoming aware of such noncompliance. If all
+Recipient's rights under this Agreement terminate, Recipient agrees
+to cease use and distribution of the Program as soon as reasonably
+practicable. However, Recipient's obligations under this Agreement
+and any licenses granted by Recipient relating to the Program shall
+continue and survive.
+
+Everyone is permitted to copy and distribute copies of this
+Agreement, but in order to avoid inconsistency the Agreement is
+copyrighted and may only be modified in the following manner. The
+Agreement Steward reserves the right to publish new versions
+(including revisions) of this Agreement from time to time. No
+one other than the Agreement Steward has the right to modify
+this Agreement. The Eclipse Foundation is the initial Agreement
+Steward. The Eclipse Foundation may assign the responsibility to
+serve as the Agreement Steward to a suitable separate entity. Each
+new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+distributed subject to the version of the Agreement under which it
+was received. In addition, after a new version of the Agreement is
+published, Contributor may elect to distribute the Program (including
+its Contributions) under the new version. Except as expressly stated
+in Sections 2(a) and 2(b) above, Recipient receives no rights or
+licenses to the intellectual property of any Contributor under
+this Agreement, whether expressly, by implication, estoppel or
+otherwise. All rights in the Program not expressly granted under
+this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and
+the intellectual property laws of the United States of America. No
+party to this Agreement will bring a legal action under this
+Agreement more than one year after the cause of action arose. Each
+party waives its rights to a jury trial in any resulting litigation.
+----
+
+----
+Export Control Classification Number (ECCN)
+
+As far as we know, the U.S. Export Control Classification Number
+(ECCN) for this software is 5D002. However, for legal reasons, we
+can make no warranty that this information is correct. For details,
+see also the Apache Software Foundation Export Classifications page.
diff --git a/lib/LICENSE-jgit b/lib/LICENSE-jgit
new file mode 100644
index 0000000..1b85c64
--- /dev/null
+++ b/lib/LICENSE-jgit
@@ -0,0 +1,37 @@
+This program and the accompanying materials are made available
+under the terms of the Eclipse Distribution License v1.0 which
+accompanies this distribution, is reproduced below, and is
+available at http://www.eclipse.org/org/documents/edl-v10.php
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the following
+conditions are met:
+
+- Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+- Neither the name of the Eclipse Foundation, Inc. nor the
+ names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior
+ written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-jsch b/lib/LICENSE-jsch
new file mode 100644
index 0000000..2cb0ddd
--- /dev/null
+++ b/lib/LICENSE-jsch
@@ -0,0 +1,26 @@
+Copyright (c) 2002-2012 Atsuhiko Yamanaka, JCraft,Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the distribution.
+
+ 3. The names of the authors may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
+INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-jsr305 b/lib/LICENSE-jsr305
new file mode 100644
index 0000000..cf30ea2
--- /dev/null
+++ b/lib/LICENSE-jsr305
@@ -0,0 +1,28 @@
+Copyright (c) 2007-2009, JSR305 expert group
+All rights reserved.
+
+http://www.opensource.org/licenses/bsd-license.php
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of the JSR305 expert group nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-ow2 b/lib/LICENSE-ow2
new file mode 100644
index 0000000..c5aba7b
--- /dev/null
+++ b/lib/LICENSE-ow2
@@ -0,0 +1,29 @@
+Copyright (c) 2000-2011 INRIA, France Telecom
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holders nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-postgresql b/lib/LICENSE-postgresql
new file mode 100644
index 0000000..fd416d2
--- /dev/null
+++ b/lib/LICENSE-postgresql
@@ -0,0 +1,26 @@
+Copyright (c) 1997-2011, PostgreSQL Global Development Group
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+3. Neither the name of the PostgreSQL Global Development Group nor the names
+ of its contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/lib/LICENSE-prologcafe b/lib/LICENSE-prologcafe
new file mode 100644
index 0000000..7183d37
--- /dev/null
+++ b/lib/LICENSE-prologcafe
@@ -0,0 +1,593 @@
+Prolog Cafe (A Prolog to Java Translator System)
+Copyright (C) 1997-2009 by Mutsunori Banbara and Naoyuki Tamura
+
+Prolog Cafe is free software; you can redistribute it and/or modify
+it under the terms of either:
+
+ * the GNU General Public License as published by the Free Software
+ Foundation; either version 2 of the License, or (at your option)
+ any later version, or
+
+ * the Eclipse Public License
+----
+
+In the context of Gerrit Code Review, Prolog Cafe is consumed under
+the <<prologcafe_EPL,EPL>>. Gerrit Code Review uses a fork derived
+from the 1.2.5 release and offers the corresponding source code at
+link:https://gerrit.googlesource.com/prolog-cafe[].
+
+----
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+----
+
+[[prologcafe_EPL]]
+----
+Eclipse Public License - v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
+OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+ documentation distributed under this Agreement, and
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from
+and are distributed by that particular Contributor. A Contribution
+'originates' from a Contributor if it was added to the Program
+by such Contributor itself or anyone acting on such Contributor's
+behalf. Contributions do not include additions to the Program which:
+(i) are separate modules of software distributed in conjunction
+with the Program under their own license agreement, and (ii) are
+not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor
+which are necessarily infringed by the use or sale of its
+Contribution alone or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with
+this Agreement.
+
+"Recipient" means anyone who receives the Program under this
+Agreement, including all Contributors.
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby
+ grants Recipient a non-exclusive, worldwide, royalty-free copyright
+ license to reproduce, prepare derivative works of, publicly display,
+ publicly perform, distribute and sublicense the Contribution of such
+ Contributor, if any, and such derivative works, in source code and
+ object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby
+ grants Recipient a non-exclusive, worldwide, royalty-free patent
+ license under Licensed Patents to make, use, sell, offer to sell,
+ import and otherwise transfer the Contribution of such Contributor,
+ if any, in source code and object code form. This patent license
+ shall apply to the combination of the Contribution and the Program
+ if, at the time the Contribution is added by the Contributor, such
+ addition of the Contribution causes such combination to be covered
+ by the Licensed Patents. The patent license shall not apply to any
+ other combinations which include the Contribution. No hardware per
+ se is licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the
+ licenses to its Contributions set forth herein, no assurances are
+ provided by any Contributor that the Program does not infringe
+ the patent or other intellectual property rights of any other
+ entity. Each Contributor disclaims any liability to Recipient
+ for claims brought by any other entity based on infringement
+ of intellectual property rights or otherwise. As a condition to
+ exercising the rights and licenses granted hereunder, each Recipient
+ hereby assumes sole responsibility to secure any other intellectual
+ property rights needed, if any. For example, if a third party patent
+ license is required to allow Recipient to distribute the Program,
+ it is Recipient's responsibility to acquire that license before
+ distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has
+ sufficient copyright rights in its Contribution, if any, to grant
+ the copyright license set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code
+ form under its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties
+ and conditions, express and implied, including warranties or
+ conditions of title and non-infringement, and implied warranties or
+ conditions of merchantability and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability
+ for damages, including direct, indirect, special, incidental and
+ consequential damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement
+ are offered by that Contributor alone and not by any other
+ party; and
+
+iv) states that source code for the Program is available from such
+ Contributor, and informs licensees how to obtain it in a reasonable
+ manner on or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained
+within the Program.
+
+Each Contributor must identify itself as the originator of its
+Contribution, if any, in a manner that reasonably allows subsequent
+Recipients to identify the originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain
+responsibilities with respect to end users, business partners and the
+like. While this license is intended to facilitate the commercial
+use of the Program, the Contributor who includes the Program in a
+commercial product offering should do so in a manner which does not
+create potential liability for other Contributors. Therefore, if a
+Contributor includes the Program in a commercial product offering,
+such Contributor ("Commercial Contributor") hereby agrees to defend
+and indemnify every other Contributor ("Indemnified Contributor")
+against any losses, damages and costs (collectively "Losses") arising
+from claims, lawsuits and other legal actions brought by a third
+party against the Indemnified Contributor to the extent caused by
+the acts or omissions of such Commercial Contributor in connection
+with its distribution of the Program in a commercial product
+offering. The obligations in this section do not apply to any claims
+or Losses relating to any actual or alleged intellectual property
+infringement. In order to qualify, an Indemnified Contributor must:
+a) promptly notify the Commercial Contributor in writing of such
+claim, and b) allow the Commercial Contributor to control, and
+cooperate with the Commercial Contributor in, the defense and any
+related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a
+commercial product offering, Product X. That Contributor is then a
+Commercial Contributor. If that Commercial Contributor then makes
+performance claims, or offers warranties related to Product X, those
+performance claims and warranties are such Commercial Contributor's
+responsibility alone. Under this section, the Commercial Contributor
+would have to defend claims against the other Contributors related
+to those performance claims and warranties, and if a court requires
+any other Contributor to pay any damages as a result, the Commercial
+Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
+PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
+WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
+OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
+responsible for determining the appropriateness of using and
+distributing the Program and assumes all risks associated with
+its exercise of rights under this Agreement , including but not
+limited to the risks and costs of program errors, compliance with
+applicable laws, damage to or loss of data, programs or equipment,
+and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
+NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY
+RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further
+action by the parties hereto, such provision shall be reformed
+to the minimum extent necessary to make such provision valid and
+enforceable.
+
+If Recipient institutes patent litigation against any entity
+(including a cross-claim or counterclaim in a lawsuit) alleging
+that the Program itself (excluding combinations of the Program with
+other software or hardware) infringes such Recipient's patent(s),
+then such Recipient's rights granted under Section 2(b) shall
+terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if
+it fails to comply with any of the material terms or conditions
+of this Agreement and does not cure such failure in a reasonable
+period of time after becoming aware of such noncompliance. If all
+Recipient's rights under this Agreement terminate, Recipient agrees
+to cease use and distribution of the Program as soon as reasonably
+practicable. However, Recipient's obligations under this Agreement
+and any licenses granted by Recipient relating to the Program shall
+continue and survive.
+
+Everyone is permitted to copy and distribute copies of this
+Agreement, but in order to avoid inconsistency the Agreement is
+copyrighted and may only be modified in the following manner. The
+Agreement Steward reserves the right to publish new versions
+(including revisions) of this Agreement from time to time. No
+one other than the Agreement Steward has the right to modify
+this Agreement. The Eclipse Foundation is the initial Agreement
+Steward. The Eclipse Foundation may assign the responsibility to
+serve as the Agreement Steward to a suitable separate entity. Each
+new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+distributed subject to the version of the Agreement under which it
+was received. In addition, after a new version of the Agreement is
+published, Contributor may elect to distribute the Program (including
+its Contributions) under the new version. Except as expressly stated
+in Sections 2(a) and 2(b) above, Recipient receives no rights or
+licenses to the intellectual property of any Contributor under
+this Agreement, whether expressly, by implication, estoppel or
+otherwise. All rights in the Program not expressly granted under
+this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and
+the intellectual property laws of the United States of America. No
+party to this Agreement will bring a legal action under this
+Agreement more than one year after the cause of action arose. Each
+party waives its rights to a jury trial in any resulting litigation.
diff --git a/lib/LICENSE-protobuf b/lib/LICENSE-protobuf
new file mode 100644
index 0000000..705db57
--- /dev/null
+++ b/lib/LICENSE-protobuf
@@ -0,0 +1,33 @@
+Copyright 2008, Google Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Code generated by the Protocol Buffer compiler is owned by the owner
+of the input file used when generating it. This code is not
+standalone and requires a support library to be linked with it. This
+support library is itself covered by the above license.
diff --git a/lib/LICENSE-slf4j b/lib/LICENSE-slf4j
new file mode 100644
index 0000000..f5ecafa
--- /dev/null
+++ b/lib/LICENSE-slf4j
@@ -0,0 +1,21 @@
+Copyright (c) 2004-2008 QOS.ch
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/antlr/BUCK b/lib/antlr/BUCK
new file mode 100644
index 0000000..732b459
--- /dev/null
+++ b/lib/antlr/BUCK
@@ -0,0 +1,48 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '3.2'
+
+maven_jar(
+ name = 'java_runtime',
+ id = 'org.antlr:antlr-runtime:' + VERSION,
+ sha1 = '31c746001016c6226bd7356c9f87a6a084ce3715',
+ license = 'antlr',
+)
+
+java_binary(
+ name = 'antlr-tool',
+ main_class = 'org.antlr.Tool',
+ deps = [':tool'],
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'stringtemplate',
+ id = 'org.antlr:stringtemplate:' + VERSION,
+ sha1 = '6fe2e3bb57daebd1555494818909f9664376dd6c',
+ license = 'antlr',
+ attach_source = False,
+ visibility = [],
+)
+
+maven_jar(
+ name = 'tool',
+ id = 'org.antlr:antlr:' + VERSION,
+ sha1 = '6b0acabea7bb3da058200a77178057e47e25cb69',
+ license = 'antlr',
+ deps = [
+ ':java_runtime',
+ ':stringtemplate',
+ ':antlr27',
+ ],
+ visibility = [],
+)
+
+maven_jar(
+ name = 'antlr27',
+ id = 'antlr:antlr:2.7.7',
+ sha1 = '83cd2cd674a217ade95a4bb83a8a14f351f48bd0',
+ license = 'antlr',
+ attach_source = False,
+ visibility = [],
+)
diff --git a/lib/asciidoctor/BUCK b/lib/asciidoctor/BUCK
new file mode 100644
index 0000000..25b9eb8
--- /dev/null
+++ b/lib/asciidoctor/BUCK
@@ -0,0 +1,39 @@
+include_defs('//lib/maven.defs')
+
+java_binary(
+ name = 'asciidoc',
+ main_class = 'org.asciidoctor.cli.AsciidoctorInvoker',
+ deps = [':asciidoctor'],
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'asciidoctor',
+ id = 'org.asciidoctor:asciidoctor-java-integration:0.1.3',
+ sha1 = '5cf21b4331d737ef0f3b3f543a7e5a343c1f27ec',
+ license = 'Apache2.0',
+ visibility = [],
+ attach_source = False,
+ deps = [
+ ':jcommander',
+ ':jruby',
+ ],
+)
+
+maven_jar(
+ name = 'jcommander',
+ id = 'com.beust:jcommander:1.30',
+ sha1 = 'c440b30a944ba199751551aee393f8aa03b3c327',
+ license = 'Apache2.0',
+ visibility = [],
+ attach_source = False,
+)
+
+maven_jar(
+ name = 'jruby',
+ id = 'org.jruby:jruby-complete:1.7.4',
+ sha1 = '74984d84846523bd7da49064679ed1ccf199e1db',
+ license = 'DO_NOT_DISTRIBUTE',
+ visibility = [],
+ attach_source = False,
+)
diff --git a/lib/bouncycastle/BUCK b/lib/bouncycastle/BUCK
new file mode 100644
index 0000000..3d76ea6
--- /dev/null
+++ b/lib/bouncycastle/BUCK
@@ -0,0 +1,20 @@
+include_defs('//lib/maven.defs')
+
+# This version must match the version that also appears in
+# gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
+VERSION = '1.44'
+
+maven_jar(
+ name = 'bcprov',
+ id = 'org.bouncycastle:bcprov-jdk16:' + VERSION,
+ sha1 = '6327a5f7a3dc45e0fd735adb5d08c5a74c05c20c',
+ license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
+)
+
+maven_jar(
+ name = 'bcpg',
+ id = 'org.bouncycastle:bcpg-jdk16:' + VERSION,
+ sha1 = 'ee14f5a29cb3cf9c1edec034ab16e1bbd26e9647',
+ license = 'DO_NOT_DISTRIBUTE', #'bouncycastle'
+ deps = [':bcprov'],
+)
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
new file mode 100644
index 0000000..b381fe2
--- /dev/null
+++ b/lib/codemirror/BUCK
@@ -0,0 +1,89 @@
+include_defs('//lib/maven.defs')
+include_defs('//lib/codemirror/cm3.defs')
+
+VERSION = '2d51b31fa7'
+SHA1 = '459bc0b701f7550a7751ea43ae33672195c7d1e0'
+URL = GERRIT + 'net/codemirror/codemirror-%s.zip' % VERSION
+
+ZIP = 'codemirror-%s.zip' % VERSION
+TOP = 'codemirror-%s' % VERSION
+
+genrule(
+ name = 'css',
+ cmd = ';'.join([
+ ':>$OUT',
+ "echo '/** @license' >>$OUT",
+ 'unzip -p $SRCDIR/%s %s/LICENSE >>$OUT' % (ZIP, TOP),
+ "echo '*/' >>$OUT",
+ ] +
+ ['unzip -p $SRCDIR/%s %s/%s >>$OUT' % (ZIP, TOP, n)
+ for n in CM3_CSS]
+ ),
+ srcs = [genfile(ZIP)],
+ deps = [':download'],
+ out = 'cm3.css',
+)
+
+# TODO(sop) Minify with Closure JavaScript compiler.
+genrule(
+ name = 'js',
+ cmd = ';'.join([
+ ':>$OUT',
+ "echo '/** @license' >>$OUT",
+ 'unzip -p $SRCDIR/%s %s/LICENSE >>$OUT' % (ZIP, TOP),
+ "echo '*/' >>$OUT",
+ ] +
+ ['unzip -p $SRCDIR/%s %s/%s >>$OUT' % (ZIP, TOP, n)
+ for n in CM3_JS]
+ ),
+ srcs = [genfile(ZIP)],
+ deps = [':download'],
+ out = 'cm3.js',
+)
+
+prebuilt_jar(
+ name = 'codemirror',
+ binary_jar = genfile('codemirror.jar'),
+ deps = [
+ ':jar',
+ '//lib:LICENSE-codemirror',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+genrule(
+ name = 'jar',
+ cmd = ';'.join([
+ 'cd $TMP',
+ 'unzip -q $SRCDIR/%s %s' % (
+ ZIP,
+ ' '.join(['%s/mode/%s' % (TOP, n) for n in CM3_MODES])),
+ 'mkdir net',
+ 'mv %s net/codemirror' % TOP,
+ 'mkdir net/codemirror/lib',
+ 'mv $SRCDIR/cm3.css net/codemirror/lib',
+ 'mv $SRCDIR/cm3.js net/codemirror/lib',
+ 'zip -qr $OUT *'
+ ]),
+ srcs = [
+ genfile(ZIP),
+ genfile('cm3.css'),
+ genfile('cm3.js'),
+ ],
+ deps = [
+ ':download',
+ ':css',
+ ':js',
+ ],
+ out = 'codemirror.jar',
+)
+
+genrule(
+ name = 'download',
+ cmd = '$(exe //tools:download_file)' +
+ ' -o $OUT' +
+ ' -u ' + URL +
+ ' -v ' + SHA1,
+ deps = ['//tools:download_file'],
+ out = 'codemirror-' + VERSION + '.zip',
+)
diff --git a/lib/codemirror/cm3.defs b/lib/codemirror/cm3.defs
new file mode 100644
index 0000000..ef1851b
--- /dev/null
+++ b/lib/codemirror/cm3.defs
@@ -0,0 +1,28 @@
+CM3_CSS = [
+ 'lib/codemirror.css',
+ 'addon/dialog/dialog.css',
+]
+
+CM3_JS = [
+ 'lib/codemirror.js',
+ 'keymap/vim.js',
+ 'addon/dialog/dialog.js',
+ 'addon/search/searchcursor.js',
+ 'addon/search/search.js',
+ 'addon/selection/mark-selection.js',
+ 'addon/edit/trailingspace.js',
+]
+
+CM3_MODES = [
+ 'clike/clike.js',
+ 'css/css.js',
+ 'go/go.js',
+ 'htmlmixed/htmlmixed.js',
+ 'javascript/javascript.js',
+ 'properties/properties.js',
+ 'python/python.js',
+ 'shell/shell.js',
+ 'sql/sql.js',
+ 'velocity/velocity.js',
+ 'xml/xml.js',
+]
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
new file mode 100644
index 0000000..fd066ef
--- /dev/null
+++ b/lib/commons/BUCK
@@ -0,0 +1,106 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+ name = 'codec',
+ id = 'commons-codec:commons-codec:1.4',
+ sha1 = '4216af16d38465bbab0f3dff8efa14204f7a399a',
+ license = 'Apache2.0',
+ exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+)
+
+maven_jar(
+ name = 'collections',
+ id = 'commons-collections:commons-collections:3.2.1',
+ sha1 = '761ea405b9b37ced573d2df0d1e3a4e0f9edc668',
+ license = 'Apache2.0',
+ exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+ attach_source = False,
+ visibility = [
+ '//lib:velocity',
+ '//lib/solr:zookeeper',
+ ],
+)
+
+maven_jar(
+ name = 'dbcp',
+ id = 'commons-dbcp:commons-dbcp:1.4',
+ sha1 = '30be73c965cc990b153a100aaaaafcf239f82d39',
+ license = 'Apache2.0',
+ deps = [':pool'],
+ exclude = [
+ 'META-INF/LICENSE.txt',
+ 'META-INF/NOTICE.txt',
+ 'testpool.jocl'
+ ],
+)
+
+maven_jar(
+ name = 'lang',
+ id = 'commons-lang:commons-lang:2.5',
+ sha1 = 'b0236b252e86419eef20c31a44579d2aee2f0a69',
+ license = 'Apache2.0',
+ exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+)
+
+maven_jar(
+ name = 'net',
+ id = 'commons-net:commons-net:2.2',
+ sha1 = '07993c12f63c78378f8c90de4bc2ee62daa7ca3a',
+ license = 'Apache2.0',
+ exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+)
+
+maven_jar(
+ name = 'pool',
+ id = 'commons-pool:commons-pool:1.5.5',
+ sha1 = '7d8ffbdc47aa0c5a8afe5dc2aaf512f369f1d19b',
+ license = 'Apache2.0',
+ attach_source = False,
+ exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+)
+
+maven_jar(
+ name = 'oro',
+ id = 'oro:oro:2.0.8',
+ sha1 = '5592374f834645c4ae250f4c9fbb314c9369d698',
+ license = 'Apache1.1',
+ attach_source = False,
+ exclude = ['META-INF/LICENSE'],
+)
+
+maven_jar(
+ name = 'io',
+ id = 'commons-io:commons-io:1.4',
+ sha1 = 'a8762d07e76cfde2395257a5da47ba7c1dbd3dce',
+ license = 'Apache2.0',
+ exclude = ['META-INF/MANIFEST.MF'],
+)
+
+maven_jar(
+ name = 'httpclient',
+ id = 'org.apache.httpcomponents:httpclient:4.2.5',
+ bin_sha1 = '666e26e76f2e87d84e4f16acb546481ae1b8e9a6',
+ src_sha1 = '55d345272944d7e8dace47925336a3764ee0e24b',
+ license = 'Apache2.0',
+ deps = [
+ ':codec',
+ ':httpcore',
+ '//lib/log:jcl-over-slf4j',
+ ],
+)
+
+maven_jar(
+ name = 'httpcore',
+ id = 'org.apache.httpcomponents:httpcore:4.2.4',
+ bin_sha1 = '3b7f38df6de5dd8b500e602ae8c2dd5ee446f883',
+ src_sha1 = 'c3ffe3a73348645042fb0b06303b6a3de194494e',
+ license = 'Apache2.0',
+)
+
+maven_jar(
+ name = 'httpmime',
+ id = 'org.apache.httpcomponents:httpmime:4.2.5',
+ bin_sha1 = '412b9914d0adec6d5716df1ada8acbc4f6f2dd37',
+ src_sha1 = 'c07ce7f6b153284a9ebaf58532c2442200cf3aa2',
+ license = 'Apache2.0',
+)
diff --git a/lib/guice/BUCK b/lib/guice/BUCK
new file mode 100644
index 0000000..4fac2c0
--- /dev/null
+++ b/lib/guice/BUCK
@@ -0,0 +1,96 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '4.0-beta'
+EXCLUDE = [
+ 'META-INF/DEPENDENCIES',
+ 'META-INF/LICENSE',
+ 'META-INF/NOTICE',
+]
+
+java_library(
+ name = 'guice',
+ deps = [
+ ':guice_library',
+ ':javax-inject',
+ ],
+ export_deps = True,
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'guice_library',
+ id = 'com.google.inject:guice:' + VERSION,
+ sha1 = 'a82be989679df08b66d48b42659a3ca2daaf1d5b',
+ license = 'Apache2.0',
+ deps = [':aopalliance'],
+ exclude = EXCLUDE + [
+ 'META-INF/maven/com.google.guava/guava/pom.properties',
+ 'META-INF/maven/com.google.guava/guava/pom.xml',
+ 'javax/annotation/CheckForNull.java',
+ 'javax/annotation/CheckForSigned.java',
+ 'javax/annotation/CheckReturnValue.java',
+ 'javax/annotation/concurrent/GuardedBy.java',
+ 'javax/annotation/concurrent/Immutable.java',
+ 'javax/annotation/concurrent/NotThreadSafe.java',
+ 'javax/annotation/concurrent/ThreadSafe.java',
+ 'javax/annotation/Detainted.java',
+ 'javax/annotation/MatchesPattern.java',
+ 'javax/annotation/meta/Exclusive.java',
+ 'javax/annotation/meta/Exhaustive.java',
+ 'javax/annotation/meta/TypeQualifier.java',
+ 'javax/annotation/meta/TypeQualifierDefault.java',
+ 'javax/annotation/meta/TypeQualifierNickname.java',
+ 'javax/annotation/meta/TypeQualifierValidator.java',
+ 'javax/annotation/meta/When.java',
+ 'javax/annotation/Nonnegative.java',
+ 'javax/annotation/Nonnull.java',
+ 'javax/annotation/Nullable.java',
+ 'javax/annotation/OverridingMethodsMustInvokeSuper.java',
+ 'javax/annotation/ParametersAreNonnullByDefault.java',
+ 'javax/annotation/ParametersAreNullableByDefault.java',
+ 'javax/annotation/PropertyKey.java',
+ 'javax/annotation/RegEx.java',
+ 'javax/annotation/Signed.java',
+ 'javax/annotation/Syntax.java',
+ 'javax/annotation/Tainted.java',
+ 'javax/annotation/Untainted.java',
+ 'javax/annotation/WillClose.java',
+ 'javax/annotation/WillCloseWhenClosed.java',
+ 'javax/annotation/WillNotClose.java',
+ ],
+ visibility = [],
+)
+
+maven_jar(
+ name = 'guice-assistedinject',
+ id = 'com.google.inject.extensions:guice-assistedinject:' + VERSION,
+ sha1 = 'abd6511011a9e4b64e2ebb60caac2e1cd6cd19a1',
+ license = 'Apache2.0',
+ deps = [':guice'],
+ exclude = EXCLUDE,
+)
+
+maven_jar(
+ name = 'guice-servlet',
+ id = 'com.google.inject.extensions:guice-servlet:' + VERSION,
+ sha1 = '46b44984f254c0bf92d0c972fad1a70292ada28e',
+ license = 'Apache2.0',
+ deps = [':guice'],
+ exclude = EXCLUDE,
+)
+
+maven_jar(
+ name = 'aopalliance',
+ id = 'aopalliance:aopalliance:1.0',
+ sha1 = '0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8',
+ license = 'PublicDomain',
+ visibility = ['//lib/guice:guice'],
+)
+
+maven_jar(
+ name = 'javax-inject',
+ id = 'javax.inject:javax.inject:1',
+ sha1 = '6975da39a7040257bd51d21a231b76c915872d38',
+ license = 'Apache2.0',
+ visibility = ['//lib/guice:guice'],
+)
diff --git a/lib/gwt/BUCK b/lib/gwt/BUCK
new file mode 100644
index 0000000..9f37c9d
--- /dev/null
+++ b/lib/gwt/BUCK
@@ -0,0 +1,60 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '2.5.0'
+
+maven_jar(
+ name = 'user',
+ id = 'com.google.gwt:gwt-user:' + VERSION,
+ sha1 = 'bbec0cc2ba81a2601aac55e3783ad0d0ff7d45ac',
+ license = 'Apache2.0',
+ attach_source = False,
+)
+
+maven_jar(
+ name = 'dev',
+ id = 'com.google.gwt:gwt-dev:' + VERSION,
+ sha1 = '3c227524e0978036e76e73e125ac8535a9e8fd7c',
+ license = 'Apache2.0',
+ deps = [
+ ':javax-validation',
+ ':javax-validation_src',
+ ],
+ attach_source = False,
+)
+
+maven_jar(
+ name = 'javax-validation',
+ id = 'javax.validation:validation-api:1.0.0.GA',
+ bin_sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e',
+ src_sha1 = '7a561191db2203550fbfa40d534d4997624cd369',
+ license = 'Apache2.0',
+ visibility = [],
+)
+
+python_binary(
+ name = 'compiler',
+ main = 'compiler.py',
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'gwt-test-utils',
+ id = 'com.googlecode.gwt-test-utils:gwt-test-utils:0.45-SNAPSHOT-20130612.054327-7',
+ sha1 = 'ad53b8a05df35fbe394db80d1d7d2913a646bfb3',
+ license = 'Apache2.0',
+ repository = 'http://oss.sonatype.org/content/repositories/snapshots',
+ deps = [
+ ':javassist',
+ '//lib/log:api',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'javassist',
+ id = 'org.javassist:javassist:3.16.1-GA',
+ sha1 = '315891b371395271977af518d4db5cee1a0bc9bf',
+ license = 'Apache2.0',
+ visibility = [],
+)
+
diff --git a/lib/gwt/compiler.py b/lib/gwt/compiler.py
new file mode 100755
index 0000000..29a8609
--- /dev/null
+++ b/lib/gwt/compiler.py
@@ -0,0 +1,61 @@
+#!/usr/bin/python
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import print_function
+
+from multiprocessing import cpu_count
+from os import environ, makedirs, mkdir, path
+from subprocess import Popen, PIPE
+from sys import argv, stderr
+
+cp, opt, end, TMP = [], [], False, environ['TMP']
+module, outzip = argv[1], argv[2]
+
+for a in argv[3:]:
+ if end:
+ if a.endswith('.jar'):
+ cp.append(path.expandvars(a))
+ elif a == '--':
+ end = True
+ else:
+ opt.append(a)
+
+if not outzip.endswith('.zip'):
+ print("%s must end with .zip" % outzip, file=stderr)
+ exit(1)
+
+for d in ['deploy', 'unit_cache', 'work']:
+ mkdir(path.join(TMP, d))
+if not path.exists(path.dirname(outzip)):
+ makedirs(path.dirname(outzip))
+
+cmd = [
+ 'java', '-Xmx512m',
+ '-Djava.io.tmpdir=' + TMP,
+ '-Dgwt.normalizeTimestamps=true',
+ '-Dgwt.persistentunitcachedir=' + path.join(TMP, 'unit_cache'),
+ '-classpath', ':'.join(cp),
+ 'com.google.gwt.dev.Compiler',
+ '-deploy', path.join(TMP, 'deploy'),
+ '-workDir', path.join(TMP, 'work'),
+ '-war', outzip,
+ '-localWorkers', str(cpu_count()),
+] + opt + [module]
+
+gwt = Popen(cmd, stdout = PIPE, stderr = PIPE)
+out, err = gwt.communicate()
+if gwt.returncode != 0:
+ print(out + err, file=stderr)
+ exit(gwt.returncode)
diff --git a/lib/jetty/BUCK b/lib/jetty/BUCK
new file mode 100644
index 0000000..6eac1a9
--- /dev/null
+++ b/lib/jetty/BUCK
@@ -0,0 +1,75 @@
+include_defs('//lib/maven.defs')
+
+VERSION = '8.1.7.v20120910'
+EXCLUDE = ['about.html']
+
+maven_jar(
+ name = 'servlet',
+ id = 'org.eclipse.jetty:jetty-servlet:' + VERSION,
+ sha1 = '93da01e3ea26e70449e9a1a0affa5c31436be5a0',
+ license = 'Apache2.0',
+ deps = [
+ ':security',
+ '//lib:servlet-api-3_0',
+ ],
+ exclude = EXCLUDE,
+)
+
+maven_jar(
+ name = 'security',
+ id = 'org.eclipse.jetty:jetty-security:' + VERSION,
+ sha1 = '8d78beb7a07f4cccee05a3f16a264f1025946258',
+ license = 'Apache2.0',
+ deps = [':server'],
+ exclude = EXCLUDE,
+ visibility = [],
+)
+
+maven_jar(
+ name = 'server',
+ id = 'org.eclipse.jetty:jetty-server:' + VERSION,
+ sha1 = '6c81f733f28713919e99c2f8952e6ca5178033cd',
+ license = 'Apache2.0',
+ deps = [
+ ':continuation',
+ ':http',
+ ],
+ export_deps = True,
+ exclude = EXCLUDE,
+)
+
+maven_jar(
+ name = 'continuation',
+ id = 'org.eclipse.jetty:jetty-continuation:' + VERSION,
+ sha1 = 'f60cfe6267038000b459508529c88737601081e4',
+ license = 'Apache2.0',
+ exclude = EXCLUDE,
+)
+
+maven_jar(
+ name = 'http',
+ id = 'org.eclipse.jetty:jetty-http:' + VERSION,
+ sha1 = '10126433876cd74534695f7f99c4362596555493',
+ license = 'Apache2.0',
+ deps = [':io'],
+ exclude = EXCLUDE,
+)
+
+maven_jar(
+ name = 'io',
+ id = 'org.eclipse.jetty:jetty-io:' + VERSION,
+ sha1 = 'a81f746ae1b10c37e1bb0a01d1374c202c0bd549',
+ license = 'Apache2.0',
+ deps = [':util'],
+ exclude = EXCLUDE,
+ visibility = [],
+)
+
+maven_jar(
+ name = 'util',
+ id = 'org.eclipse.jetty:jetty-util:' + VERSION,
+ sha1 = '7eb2004ab2c22fd3b00095bd9ba0f32a9e88f6a5',
+ license = 'Apache2.0',
+ exclude = EXCLUDE,
+ visibility = [],
+)
diff --git a/lib/jgit/BUCK b/lib/jgit/BUCK
new file mode 100644
index 0000000..b8ff017
--- /dev/null
+++ b/lib/jgit/BUCK
@@ -0,0 +1,65 @@
+include_defs('//lib/maven.defs')
+
+REPO = GERRIT
+VERS = '3.0.0.201306101825-r.41-g84d2738'
+
+maven_jar(
+ name = 'jgit',
+ id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
+ bin_sha1 = 'fec8584e9e60ab3f43c3dd136f72662818e07766',
+ src_sha1 = '398e49984b281dbcd913b3894b8b9582c8b2cf9a',
+ license = 'jgit',
+ repository = REPO,
+ deps = [':ewah'],
+ exclude = [
+ 'META-INF/eclipse.inf',
+ 'about.html',
+ 'plugin.properties',
+ ],
+)
+
+maven_jar(
+ name = 'jgit-servlet',
+ id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
+ sha1 = 'b20c36982aa90fc08180ef4a570bf28de5e1a0ab',
+ license = 'jgit',
+ repository = REPO,
+ deps = [':jgit'],
+ exclude = [
+ 'about.html',
+ 'plugin.properties',
+ ],
+)
+
+maven_jar(
+ name = 'junit',
+ id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
+ sha1 = 'ee899f2d96b51e400ae84cd9405330a7e8ce1cb9',
+ license = 'DO_NOT_DISTRIBUTE',
+ repository = REPO,
+ deps = [':jgit'],
+)
+
+maven_jar(
+ name = 'ewah',
+ id = 'com.googlecode.javaewah:JavaEWAH:0.5.6',
+ sha1 = '1207c0fc8552d4f5f574b50f29321d923521128e',
+ license = 'Apache2.0',
+)
+
+prebuilt_jar(
+ name = 'Edit',
+ binary_jar = genfile('edit-src.jar'),
+ deps = [':jgit_edit_src'],
+ visibility = ['PUBLIC'],
+)
+
+genrule(
+ name = 'jgit_edit_src',
+ cmd = 'unzip -qd $TMP $SRCS org/eclipse/jgit/diff/Edit.java;' +
+ 'cd $TMP;' +
+ 'zip -Dq $OUT org/eclipse/jgit/diff/Edit.java',
+ srcs = [genfile('jgit/org.eclipse.jgit-%s-src.jar' % VERS)],
+ out = 'edit-src.jar',
+ deps = [':jgit_src']
+)
diff --git a/lib/log/BUCK b/lib/log/BUCK
new file mode 100644
index 0000000..2659fcd
--- /dev/null
+++ b/lib/log/BUCK
@@ -0,0 +1,31 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+ name = 'api',
+ id = 'org.slf4j:slf4j-api:1.6.1',
+ sha1 = '6f3b8a24bf970f17289b234284c94f43eb42f0e4',
+ license = 'slf4j',
+)
+
+maven_jar(
+ name = 'impl_log4j',
+ id = 'org.slf4j:slf4j-log4j12:1.6.1',
+ sha1 = 'bd245d6746cdd4e6203e976e21d597a46f115802',
+ license = 'slf4j',
+ deps = [':log4j'],
+)
+
+maven_jar(
+ name = 'log4j',
+ id = 'log4j:log4j:1.2.16',
+ sha1 = '7999a63bfccbc7c247a9aea10d83d4272bd492c6',
+ license = 'Apache2.0',
+ exclude = ['META-INF/LICENSE', 'META-INF/NOTICE'],
+)
+
+maven_jar(
+ name = 'jcl-over-slf4j',
+ id = 'org.slf4j:jcl-over-slf4j:1.6.1',
+ sha1 = '99c61095a14dfc9e47a086068033c286bf236475',
+ license = 'slf4j',
+)
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
new file mode 100644
index 0000000..e0972d9
--- /dev/null
+++ b/lib/lucene/BUCK
@@ -0,0 +1,49 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+ name = 'core',
+ id = 'org.apache.lucene:lucene-core:4.3.0',
+ bin_sha1 = 'd4e40fe5661b8de5d8c66db3d63a47b6b3ecf7f3',
+ src_sha1 = '86c29288b1930e33ba7ffea1b866af9a52d3d24a',
+ license = 'Apache2.0',
+ exclude = [
+ 'META-INF/LICENSE.txt',
+ 'META-INF/NOTICE.txt',
+ ],
+)
+
+maven_jar(
+ name = 'analyzers-common',
+ id = 'org.apache.lucene:lucene-analyzers-common:4.3.0',
+ bin_sha1 = 'e7c3976156d292f696016e138b67ab5e6bfc1a56',
+ src_sha1 = '3606622b3c1f09b4b7cf34070cbf60d414af9b6b',
+ license = 'Apache2.0',
+ exclude = [
+ 'META-INF/LICENSE.txt',
+ 'META-INF/NOTICE.txt',
+ ],
+)
+
+maven_jar(
+ name = 'highlighter',
+ id = 'org.apache.lucene:lucene-highlighter:4.3.0',
+ bin_sha1 = '9e6d60921e16a0d6b2e609c6a02a8b08cd7f643c',
+ src_sha1 = '0ff70cae1a8fb7af29bf254d90e9885961deed5e',
+ license = 'Apache2.0',
+)
+
+maven_jar(
+ name = 'queries',
+ id = 'org.apache.lucene:lucene-queries:4.3.0',
+ bin_sha1 = '68e01022bdf4f869b95362c9af964846e5d3cf2d',
+ src_sha1 = '3e3541c1b9f44c532ce88ab6a12216566c3399df',
+ license = 'Apache2.0',
+)
+
+maven_jar(
+ name = 'spellchecker',
+ id = 'org.apache.lucene:lucene-spellchecker:3.6.2',
+ bin_sha1 = '15db0c0cfee44e275f15ad046e46b9a05910ad24',
+ src_sha1 = 'bbecb3fb725ae594101c165a72c102296007c203',
+ license = 'Apache2.0',
+)
diff --git a/lib/maven.defs b/lib/maven.defs
new file mode 100644
index 0000000..db37cdb
--- /dev/null
+++ b/lib/maven.defs
@@ -0,0 +1,128 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+GERRIT = 'GERRIT:'
+MAVEN_CENTRAL = 'MAVEN_CENTRAL:'
+MAVEN_LOCAL = 'MAVEN_LOCAL:'
+
+def define_license(name):
+ n = 'LICENSE-' + name
+ genrule(
+ name = n,
+ cmd = 'ln -s $SRCS $OUT',
+ srcs = [n],
+ out = n,
+ visibility = ['PUBLIC'],
+ )
+
+def maven_jar(
+ name,
+ id,
+ license,
+ exclude = [],
+ exclude_java_sources = False,
+ deps = [],
+ sha1 = '', bin_sha1 = '', src_sha1 = '',
+ repository = MAVEN_CENTRAL,
+ attach_source = True,
+ export_deps = False,
+ visibility = ['PUBLIC']):
+ from os import path
+
+ parts = id.split(':')
+ if len(parts) != 3:
+ raise NameError('expected id="groupId:artifactId:version"')
+ group, artifact, version = parts
+
+ if 'SNAPSHOT' in version:
+ file_version = version.replace('-SNAPSHOT', '')
+ version = version.split('-')[0] + '-SNAPSHOT'
+ else:
+ file_version = version
+
+ jar = path.join(name, artifact.lower() + '-' + file_version)
+ url = '/'.join([
+ repository,
+ group.replace('.', '/'), artifact, version,
+ artifact + '-' + file_version])
+
+ binjar = jar + '.jar'
+ binurl = url + '.jar'
+
+ srcjar = jar + '-src.jar'
+ srcurl = url + '-sources.jar'
+
+ cmd = ['$(exe //tools:download_file)', '-o', '$OUT', '-u', binurl]
+ if sha1:
+ cmd.extend(['-v', sha1])
+ elif bin_sha1:
+ cmd.extend(['-v', bin_sha1])
+ for x in exclude:
+ cmd.extend(['-x', x])
+ if exclude_java_sources:
+ cmd.append('--exclude_java_sources')
+
+ genrule(
+ name = name + '__download_bin',
+ cmd = ' '.join(cmd),
+ deps = ['//tools:download_file'],
+ out = binjar,
+ )
+ license = ['//lib:LICENSE-' + license]
+
+ if src_sha1 or attach_source:
+ cmd = ['$(exe //tools:download_file)', '-o', '$OUT', '-u', srcurl]
+ if src_sha1:
+ cmd.extend(['-v', src_sha1])
+ genrule(
+ name = name + '__download_src',
+ cmd = ' '.join(cmd),
+ deps = ['//tools:download_file'],
+ out = srcjar,
+ )
+ prebuilt_jar(
+ name = name + '_src',
+ binary_jar = genfile(srcjar),
+ deps = license + [':' + name + '__download_src'],
+ visibility = visibility,
+ )
+ else:
+ srcjar = None
+ genrule(
+ name = name + '__download_src',
+ cmd = ':>$OUT',
+ out = '__' + name + '__no_src',
+ )
+
+ if export_deps:
+ prebuilt_jar(
+ name = name + '__jar',
+ deps = deps + license + [':' + name + '__download_bin'],
+ binary_jar = genfile(binjar),
+ source_jar = genfile(srcjar) if srcjar else None,
+ )
+ java_library(
+ name = name,
+ deps = [':' + name + '__jar'],
+ export_deps = True,
+ visibility = visibility,
+ )
+ else:
+ prebuilt_jar(
+ name = name,
+ deps = deps + license + [':' + name + '__download_bin'],
+ binary_jar = genfile(binjar),
+ source_jar = genfile(srcjar) if srcjar else None,
+ visibility = visibility,
+ )
diff --git a/lib/mina/BUCK b/lib/mina/BUCK
new file mode 100644
index 0000000..3e9558a
--- /dev/null
+++ b/lib/mina/BUCK
@@ -0,0 +1,24 @@
+include_defs('//lib/maven.defs')
+
+EXCLUDE = [
+ 'META-INF/DEPENDENCIES',
+ 'META-INF/LICENSE',
+ 'META-INF/NOTICE',
+]
+
+maven_jar(
+ name = 'core',
+ id = 'org.apache.mina:mina-core:2.0.5',
+ sha1 = '0e134a3761833a3c28c79331e806f64f985a9eec',
+ license = 'Apache2.0',
+ exclude = EXCLUDE,
+)
+
+maven_jar(
+ name = 'sshd',
+ id = 'org.apache.sshd:sshd-core:0.6.0',
+ sha1 = '2b9a119dd77a1decec78b0c511ba400c8655e96e',
+ license = 'Apache2.0',
+ deps = [':core'],
+ exclude = EXCLUDE,
+)
diff --git a/lib/openid/BUCK b/lib/openid/BUCK
new file mode 100644
index 0000000..c6c8baf
--- /dev/null
+++ b/lib/openid/BUCK
@@ -0,0 +1,35 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+ name = 'consumer',
+ id = 'org.openid4java:openid4java:0.9.8',
+ sha1 = 'de4f1b33d3b0f0b2ab1d32834ec1190b39db4160',
+ license = 'Apache2.0',
+ deps = [
+ ':nekohtml',
+ ':xerces',
+ '//lib/commons:httpclient',
+ '//lib/log:jcl-over-slf4j',
+ '//lib/guice:guice',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+maven_jar(
+ name = 'nekohtml',
+ id = 'net.sourceforge.nekohtml:nekohtml:1.9.10',
+ sha1 = '14052461031a7054aa094f5573792feb6686d3de',
+ license = 'Apache2.0',
+ deps = [':xerces'],
+ attach_source = False,
+ visibility = [],
+)
+
+maven_jar(
+ name = 'xerces',
+ id = 'xerces:xercesImpl:2.8.1',
+ sha1 = '25101e37ec0c907db6f0612cbf106ee519c1aef1',
+ license = 'Apache2.0',
+ attach_source = False,
+ visibility = [],
+)
diff --git a/lib/prolog/BUCK b/lib/prolog/BUCK
new file mode 100644
index 0000000..1f3e425
--- /dev/null
+++ b/lib/prolog/BUCK
@@ -0,0 +1,23 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+ name = 'prolog-cafe',
+ id = 'com.googlecode.prolog-cafe:PrologCafe:1.3',
+ sha1 = '5e0fbf18e8c98c4113f9acc978306884a1152870',
+ license = 'prologcafe',
+ repository = GERRIT,
+)
+
+java_binary(
+ name = 'compiler',
+ main_class = 'BuckPrologCompiler',
+ deps = [':compiler_lib'],
+ visibility = ['PUBLIC'],
+)
+
+java_library(
+ name = 'compiler_lib',
+ srcs = ['java/BuckPrologCompiler.java'],
+ deps = [':prolog-cafe'],
+ visibility = ['//tools/eclipse:classpath'],
+)
diff --git a/lib/prolog/java/BuckPrologCompiler.java b/lib/prolog/java/BuckPrologCompiler.java
new file mode 100644
index 0000000..0db6763
--- /dev/null
+++ b/lib/prolog/java/BuckPrologCompiler.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.compiler.Compiler;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+public class BuckPrologCompiler {
+ public static void main(String[] argv) throws IOException, CompileException {
+ File out = new File(argv[argv.length - 1]);
+ File java = tmpdir("java");
+ for (int i = 0; i < argv.length - 1; i++) {
+ File src = new File(argv[i]);
+ new Compiler().prologToJavaSource(src.getPath(), java.getPath());
+ }
+ jar(out, java);
+ }
+
+ private static File tmpdir(String name) throws IOException {
+ File d = File.createTempFile(name + "_", "");
+ if (!d.delete() || !d.mkdir()) {
+ throw new IOException("Cannot mkdir " + d);
+ }
+ return d;
+ }
+
+ private static void jar(File jar, File classes) throws IOException {
+ File tmp = File.createTempFile("prolog", ".jar", jar.getParentFile());
+ try {
+ JarOutputStream out = new JarOutputStream(new FileOutputStream(tmp));
+ try {
+ add(out, classes, "");
+ } finally {
+ out.close();
+ }
+ if (!tmp.renameTo(jar)) {
+ throw new IOException("Cannot create " + jar);
+ }
+ } finally {
+ tmp.delete();
+ }
+ }
+
+ private static void add(JarOutputStream out, File classes, String prefix)
+ throws IOException {
+ for (String name : classes.list()) {
+ File f = new File(classes, name);
+ if (f.isDirectory()) {
+ add(out, f, prefix + name + "/");
+ continue;
+ }
+
+ JarEntry e = new JarEntry(prefix + name);
+ FileInputStream in = new FileInputStream(f);
+ try {
+ e.setTime(f.lastModified());
+ out.putNextEntry(e);
+ byte[] buf = new byte[16 << 10];
+ int n;
+ while (0 < (n = in.read(buf))) {
+ out.write(buf, 0, n);
+ }
+ } finally {
+ in.close();
+ out.closeEntry();
+ }
+ }
+ }
+}
diff --git a/lib/prolog/prolog.defs b/lib/prolog/prolog.defs
new file mode 100644
index 0000000..d8df8f7
--- /dev/null
+++ b/lib/prolog/prolog.defs
@@ -0,0 +1,48 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+def prolog_cafe_library(
+ name,
+ srcs,
+ deps = [],
+ visibility = []):
+ genrule(
+ name = name + '__pl2j',
+ cmd = 'cd $SRCDIR;$(exe //lib/prolog:compiler) ' +
+ ' '.join(srcs) +
+ ' $OUT',
+ srcs = srcs,
+ deps = ['//lib/prolog:compiler'],
+ out = name + '.src.zip',
+ )
+ java_library(
+ name = name + '__lib',
+ srcs = [genfile(name + '.src.zip')],
+ deps = [
+ ':' + name + '__pl2j',
+ '//lib/prolog:prolog-cafe',
+ ] + deps,
+ )
+ genrule(
+ name = name + '__ln',
+ cmd = 'ln -s $(location :%s__lib) $OUT' % name,
+ deps = [':%s__lib' % name],
+ out = name + '.jar',
+ )
+ prebuilt_jar(
+ name = name,
+ binary_jar = genfile(name + '.jar'),
+ deps = [':%s__ln' % name],
+ visibility = visibility,
+ )
diff --git a/lib/solr/BUCK b/lib/solr/BUCK
new file mode 100644
index 0000000..afaa948
--- /dev/null
+++ b/lib/solr/BUCK
@@ -0,0 +1,33 @@
+include_defs('//lib/maven.defs')
+
+# Java client library to use Solr over the network.
+maven_jar(
+ name = 'solrj',
+ id = 'org.apache.solr:solr-solrj:4.3.1',
+ sha1 = '433fe37796e67eaeb4452f69eb1fae2de27cb7a8',
+ license = 'Apache2.0',
+ deps = [
+ ':noggit',
+ ':zookeeper',
+ '//lib/commons:httpclient',
+ '//lib/commons:httpmime',
+ '//lib/commons:io',
+ ],
+)
+
+maven_jar(
+ name = 'noggit',
+ id = 'org.noggit:noggit:0.5',
+ sha1 = '8e6e65624d2e09a30190c6434abe23b7d4e5413c',
+ license = 'Apache2.0',
+ visibility = [],
+)
+
+maven_jar(
+ name = 'zookeeper',
+ id = 'org.apache.zookeeper:zookeeper:3.4.5',
+ sha1 = 'c0f69fb36526552a8f0bc548a6c33c49cf08e562',
+ license = 'Apache2.0',
+ deps = ['//lib/log:api'],
+ visibility = [],
+)
diff --git a/plugins/BUCK b/plugins/BUCK
new file mode 100644
index 0000000..ded6ba2
--- /dev/null
+++ b/plugins/BUCK
@@ -0,0 +1,36 @@
+BASE = get_base_path()
+CORE = [
+ 'commit-message-length-validator',
+ 'replication',
+ 'reviewnotes',
+]
+
+# buck audit parses and resolves all deps even if not reachable
+# from the root(s) passed to audit. Filter dependencies to only
+# the ones that currently exist to allow buck to parse cleanly.
+# TODO(sop): buck should more lazily resolve deps
+def filter(names):
+ from os import path
+ h, n = [], []
+ for p in names:
+ if path.exists(path.join(BASE, p, 'BUCK')):
+ h.append(p)
+ else:
+ n.append(p)
+ return h, n
+HAVE, NEED = filter(CORE)
+
+genrule(
+ name = 'core',
+ cmd = '' +
+ ';'.join(['echo >&2 plugins/'+n+' is required.' for n in NEED]) +
+ (';echo >&2;exit 1;' if NEED else '') +
+ 'mkdir -p $TMP/WEB-INF/plugins;' +
+ 'for s in $SRCS;do ln -s $s $TMP/WEB-INF/plugins;done;' +
+ 'cd $TMP;' +
+ 'zip -qr $OUT .',
+ srcs = [genfile('%s/%s.jar' % (n, n)) for n in CORE],
+ deps = ['//%s/%s:%s' % (BASE, n, n) for n in HAVE],
+ out = 'core.zip',
+ visibility = ['//:release'],
+)
diff --git a/plugins/commit-message-length-validator b/plugins/commit-message-length-validator
index 23bde32..45f347d 160000
--- a/plugins/commit-message-length-validator
+++ b/plugins/commit-message-length-validator
@@ -1 +1 @@
-Subproject commit 23bde32a6f67ef39f2a368851e3abbedc50db710
+Subproject commit 45f347d0e258ef7b871b046bfa96b9f902063b10
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
new file mode 160000
index 0000000..ce6467a
--- /dev/null
+++ b/plugins/cookbook-plugin
@@ -0,0 +1 @@
+Subproject commit ce6467a8e68ddfc564a106c63e3cf0f92a071229
diff --git a/plugins/replication b/plugins/replication
index df8cb39..d4871d5 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit df8cb393676880b6e27b42d4b49492be0804c0ee
+Subproject commit d4871d5df64d6968f2b256766dc8fa9a8133fdac
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 41fdacc..d470050 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 41fdacc58c59c35b456197d356bd6f12f6897d8e
+Subproject commit d47005095fcf3bc918a1dda62d21988cc6e470e6
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index 20310cf..0000000
--- a/pom.xml
+++ /dev/null
@@ -1,902 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2008 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
- <groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-parent</artifactId>
- <packaging>pom</packaging>
- <version>2.7</version>
-
- <name>Gerrit Code Review - Parent</name>
- <url>http://code.google.com/p/gerrit/</url>
-
- <description>
- Gerrit - Web Based Code Review
- </description>
-
- <mailingLists>
- <mailingList>
- <name>repo-discuss mailing list</name>
- <post>repo-discuss@googlegroups.com</post>
- <archive>http://groups.google.com/group/repo-discuss</archive>
- <subscribe>http://groups.google.com/group/repo-discuss/subscribe</subscribe>
- </mailingList>
- </mailingLists>
-
- <issueManagement>
- <url>http://code.google.com/p/gerrit/issues/list</url>
- <system>Google Code</system>
- </issueManagement>
-
- <properties>
- <jgitVersion>2.3.1.201302201838-r.209-g18030f9</jgitVersion>
- <gwtormVersion>1.6</gwtormVersion>
- <gwtjsonrpcVersion>1.3</gwtjsonrpcVersion>
- <gwtVersion>2.5.0</gwtVersion>
- <bouncyCastleVersion>140</bouncyCastleVersion>
- <slf4jVersion>1.6.1</slf4jVersion>
- <guiceVersion>3.0</guiceVersion>
- <jettyVersion>8.1.7.v20120910</jettyVersion>
-
- <gwt.compileReport>false</gwt.compileReport>
-
- <project.build.sourceEncoding>
- UTF-8
- </project.build.sourceEncoding>
- <project.reporting.outputEncoding>
- UTF-8
- </project.reporting.outputEncoding>
- </properties>
-
- <modules>
- <module>gerrit-patch-commonsnet</module>
- <module>gerrit-patch-jgit</module>
-
- <module>gerrit-util-cli</module>
- <module>gerrit-util-ssl</module>
-
- <module>gerrit-antlr</module>
- <module>gerrit-common</module>
- <module>gerrit-cache-h2</module>
- <module>gerrit-httpd</module>
- <module>gerrit-gwtexpui</module>
- <module>gerrit-gwtui</module>
- <module>gerrit-launcher</module>
- <module>gerrit-main</module>
- <module>gerrit-openid</module>
- <module>gerrit-pgm</module>
- <module>gerrit-prettify</module>
- <module>gerrit-reviewdb</module>
- <module>gerrit-server</module>
- <module>gerrit-sshd</module>
- <module>gerrit-gwtdebug</module>
- <module>gerrit-war</module>
-
- <module>gerrit-acceptance-tests</module>
- <module>gerrit-extension-api</module>
- <module>gerrit-plugin-api</module>
- <module>gerrit-plugin-archetype</module>
- <module>gerrit-plugin-gwtui</module>
- <module>gerrit-plugin-js-archetype</module>
- <module>gerrit-plugin-gwt-archetype</module>
- </modules>
-
- <profiles>
- <profile>
- <id>plugins</id>
- <activation>
- <property>
- <name>!gerrit.plugins.skip</name>
- </property>
- </activation>
- <modules>
- <!-- CORE PLUGIN LIST -->
- <module>plugins/commit-message-length-validator</module>
- <module>plugins/replication</module>
- <module>plugins/reviewnotes</module>
- </modules>
- </profile>
- </profiles>
-
- <licenses>
- <license>
- <name>Apache License, 2.0</name>
- <comments>
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- </comments>
- </license>
- </licenses>
-
- <build>
- <pluginManagement>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-jar-plugin</artifactId>
- <configuration>
- <archive>
- <manifestEntries>
- <Implementation-Title>Gerrit Code Review - ${project.artifactId}</Implementation-Title>
- <Implementation-Version>${project.version}</Implementation-Version>
- <Implementation-Vendor>Gerrit Code Review</Implementation-Vendor>
- <Implementation-Vendor-Id>com.google.gerrit</Implementation-Vendor-Id>
- <Implementation-Vendor-URL>http://code.google.com/p/gerrit/</Implementation-Vendor-URL>
- </manifestEntries>
- </archive>
- </configuration>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <version>2.3.2</version>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-source-plugin</artifactId>
- <version>2.1.2</version>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-shade-plugin</artifactId>
- <version>1.6</version>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-antrun-plugin</artifactId>
- <version>1.6</version>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-war-plugin</artifactId>
- <version>2.1.1</version>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-dependency-plugin</artifactId>
- <version>2.5.1</version>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-surefire-plugin</artifactId>
- <version>2.13</version>
- </plugin>
-
- <plugin>
- <groupId>org.antlr</groupId>
- <artifactId>antlr3-maven-plugin</artifactId>
- <version>3.2</version>
- </plugin>
-
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>gwt-maven-plugin</artifactId>
- <version>2.5.0</version>
- </plugin>
-
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>build-helper-maven-plugin</artifactId>
- <version>1.5</version>
- </plugin>
-
- <!--This plugin's configuration is used to store Eclipse
- m2e settings only. It has no influence on the Maven build itself. -->
- <plugin>
- <groupId>org.eclipse.m2e</groupId>
- <artifactId>lifecycle-mapping</artifactId>
- <version>1.0.0</version>
- <configuration>
- <lifecycleMappingMetadata>
- <pluginExecutions>
- <pluginExecution>
- <pluginExecutionFilter>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-antrun-plugin</artifactId>
- <versionRange>[1.0,)</versionRange>
- <goals>
- <goal>run</goal>
- </goals>
- </pluginExecutionFilter>
- <action>
- <execute>
- <runOnIncremental>false</runOnIncremental>
- <runOnConfiguration>true</runOnConfiguration>
- </execute>
- </action>
- </pluginExecution>
- <pluginExecution>
- <pluginExecutionFilter>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>build-helper-maven-plugin</artifactId>
- <versionRange>[1.0,)</versionRange>
- <goals>
- <goal>add-source</goal>
- </goals>
- </pluginExecutionFilter>
- <action>
- <execute>
- <runOnIncremental>false</runOnIncremental>
- <runOnConfiguration>true</runOnConfiguration>
- </execute>
- </action>
- </pluginExecution>
- </pluginExecutions>
- </lifecycleMappingMetadata>
- </configuration>
- </plugin>
- </plugins>
- </pluginManagement>
-
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <configuration>
- <source>1.6</source>
- <target>1.6</target>
- <encoding>UTF-8</encoding>
- </configuration>
- </plugin>
- </plugins>
-
- <extensions>
- <extension>
- <groupId>com.googlesource.gerrit</groupId>
- <artifactId>gs-maven-wagon</artifactId>
- <version>3.3</version>
- </extension>
- </extensions>
- </build>
-
- <dependencies>
- <dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <scope>test</scope>
- </dependency>
-
- <dependency>
- <groupId>org.easymock</groupId>
- <artifactId>easymock</artifactId>
- <scope>test</scope>
- </dependency>
- </dependencies>
-
- <dependencyManagement>
- <dependencies>
- <dependency>
- <groupId>com.google.code.gson</groupId>
- <artifactId>gson</artifactId>
- <version>2.1</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- <version>14.0</version>
- </dependency>
-
- <dependency>
- <groupId>gwtorm</groupId>
- <artifactId>gwtorm</artifactId>
- <version>${gwtormVersion}</version>
- </dependency>
- <dependency>
- <groupId>gwtorm</groupId>
- <artifactId>gwtorm</artifactId>
- <version>${gwtormVersion}</version>
- <classifier>sources</classifier>
- </dependency>
-
- <dependency>
- <groupId>gwtjsonrpc</groupId>
- <artifactId>gwtjsonrpc</artifactId>
- <version>${gwtjsonrpcVersion}</version>
- </dependency>
- <dependency>
- <groupId>gwtjsonrpc</groupId>
- <artifactId>gwtjsonrpc</artifactId>
- <version>${gwtjsonrpcVersion}</version>
- <classifier>sources</classifier>
- </dependency>
-
- <dependency>
- <groupId>org.openid4java</groupId>
- <artifactId>openid4java</artifactId>
- <version>0.9.8</version>
- <exclusions>
- <exclusion>
- <!-- conflicts with our use of guice 3.0 -->
- <groupId>com.google.code.guice</groupId>
- <artifactId>guice</artifactId>
- </exclusion>
- <exclusion>
- <!-- jug-1.1 is LGPL, and the source has been lost -->
- <groupId>jug</groupId>
- <artifactId>jug</artifactId>
- </exclusion>
- <exclusion>
- <!-- not required on Java 5 or later -->
- <groupId>xml-apis</groupId>
- <artifactId>xml-apis</artifactId>
- </exclusion>
-
- <!-- optional, we aren't bothering with XRI support -->
- <exclusion>
- <groupId>org.openxri</groupId>
- <artifactId>openxri</artifactId>
- </exclusion>
- <exclusion>
- <groupId>xml-security</groupId>
- <artifactId>xmlsec</artifactId>
- </exclusion>
- <exclusion>
- <groupId>xalan</groupId>
- <artifactId>xalan</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>org.apache.mina</groupId>
- <artifactId>mina-core</artifactId>
- <version>2.0.5</version>
- </dependency>
-
- <dependency>
- <groupId>org.apache.sshd</groupId>
- <artifactId>sshd-core</artifactId>
- <version>0.6.0</version>
- </dependency>
-
- <dependency>
- <groupId>com.jcraft</groupId>
- <artifactId>jsch</artifactId>
- <version>0.1.44-1</version>
- </dependency>
-
- <dependency>
- <groupId>org.apache.velocity</groupId>
- <artifactId>velocity</artifactId>
- <version>1.6.4</version>
- </dependency>
-
- <dependency>
- <groupId>args4j</groupId>
- <artifactId>args4j</artifactId>
- <version>2.0.16</version>
- </dependency>
-
- <dependency>
- <groupId>javax.validation</groupId>
- <artifactId>validation-api</artifactId>
- <version>1.0.0.GA</version>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>javax.validation</groupId>
- <artifactId>validation-api</artifactId>
- <version>1.0.0.GA</version>
- <classifier>sources</classifier>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.inject</groupId>
- <artifactId>guice</artifactId>
- <version>${guiceVersion}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.inject.extensions</groupId>
- <artifactId>guice-servlet</artifactId>
- <version>${guiceVersion}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.inject.extensions</groupId>
- <artifactId>guice-assistedinject</artifactId>
- <version>${guiceVersion}</version>
- </dependency>
-
- <dependency>
- <!-- required by Guice (whose POM is fake and lacks it) -->
- <groupId>aopalliance</groupId>
- <artifactId>aopalliance</artifactId>
- <version>1.0</version>
- </dependency>
-
- <dependency>
- <groupId>commons-net</groupId>
- <artifactId>commons-net</artifactId>
- <version>2.2</version>
- </dependency>
-
- <dependency>
- <groupId>commons-codec</groupId>
- <artifactId>commons-codec</artifactId>
- <version>1.4</version>
- </dependency>
-
- <dependency>
- <groupId>commons-dbcp</groupId>
- <artifactId>commons-dbcp</artifactId>
- <version>1.4</version>
- </dependency>
-
- <dependency>
- <groupId>commons-pool</groupId>
- <artifactId>commons-pool</artifactId>
- <version>1.5.5</version>
- </dependency>
-
- <dependency>
- <groupId>commons-lang</groupId>
- <artifactId>commons-lang</artifactId>
- <version>2.5</version>
- </dependency>
-
- <dependency>
- <groupId>eu.medsea.mimeutil</groupId>
- <artifactId>mime-util</artifactId>
- <version>2.1.3</version>
- <exclusions>
- <exclusion>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-log4j12</artifactId>
- </exclusion>
- <exclusion>
- <groupId>log4j</groupId>
- <artifactId>log4j</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>org.antlr</groupId>
- <artifactId>antlr</artifactId>
- <version>3.2</version>
- <exclusions>
- <exclusion>
- <groupId>org.antlr</groupId>
- <artifactId>stringtemplate</artifactId>
- </exclusion>
- <exclusion>
- <groupId>antlr</groupId>
- <artifactId>antlr</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>bouncycastle</groupId>
- <artifactId>bcpg-jdk15</artifactId>
- <version>${bouncyCastleVersion}</version>
- </dependency>
-
- <dependency>
- <groupId>bouncycastle</groupId>
- <artifactId>bcprov-jdk15</artifactId>
- <version>${bouncyCastleVersion}</version>
- </dependency>
-
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- <version>${slf4jVersion}</version>
- </dependency>
-
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-log4j12</artifactId>
- <version>${slf4jVersion}</version>
- </dependency>
-
- <dependency>
- <groupId>log4j</groupId>
- <artifactId>log4j</artifactId>
- <version>1.2.16</version>
- <exclusions>
- <exclusion>
- <groupId>javax.mail</groupId>
- <artifactId>mail</artifactId>
- </exclusion>
- <exclusion>
- <groupId>javax.jms</groupId>
- <artifactId>jms</artifactId>
- </exclusion>
- <exclusion>
- <groupId>com.sun.jdmk</groupId>
- <artifactId>jmxtools</artifactId>
- </exclusion>
- <exclusion>
- <groupId>com.sun.jmx</groupId>
- <artifactId>jmxri</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit</artifactId>
- <version>${jgitVersion}</version>
- </dependency>
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit</artifactId>
- <version>${jgitVersion}</version>
- <classifier>sources</classifier>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit.junit</artifactId>
- <version>${jgitVersion}</version>
- <scope>test</scope>
- <exclusions>
- <exclusion>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jgit</groupId>
- <artifactId>org.eclipse.jgit.http.server</artifactId>
- <version>${jgitVersion}</version>
- </dependency>
-
- <dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <version>4.11</version>
- </dependency>
-
- <dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <version>1.3.168</version>
- </dependency>
-
- <dependency>
- <groupId>postgresql</groupId>
- <artifactId>postgresql</artifactId>
- <version>9.1-901-1.jdbc4</version>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-servlet</artifactId>
- <version>${jettyVersion}</version>
- <exclusions>
- <exclusion>
- <!-- use Apache javax.servlet not CDDL -->
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>org.easymock</groupId>
- <artifactId>easymock</artifactId>
- <version>3.0</version>
- </dependency>
-
- <dependency>
- <groupId>org.apache.tomcat</groupId>
- <artifactId>servlet-api</artifactId>
- <version>6.0.29</version>
- </dependency>
-
- <dependency>
- <groupId>org.apache.tomcat</groupId>
- <artifactId>tomcat-servlet-api</artifactId>
- <version>7.0.32</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gwt</groupId>
- <artifactId>gwt-servlet</artifactId>
- <version>${gwtVersion}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gwt</groupId>
- <artifactId>gwt-user</artifactId>
- <version>${gwtVersion}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.gwt</groupId>
- <artifactId>gwt-dev</artifactId>
- <version>${gwtVersion}</version>
- </dependency>
-
- <dependency>
- <groupId>com.google.code.findbugs</groupId>
- <artifactId>jsr305</artifactId>
- <version>1.3.9</version>
- </dependency>
-
- <dependency>
- <groupId>com.googlecode.juniversalchardet</groupId>
- <artifactId>juniversalchardet</artifactId>
- <version>1.0.3</version>
- </dependency>
-
- <dependency>
- <groupId>dk.brics.automaton</groupId>
- <artifactId>automaton</artifactId>
- <version>1.11.8</version>
- </dependency>
-
- <dependency>
- <groupId>com.googlecode.prolog-cafe</groupId>
- <artifactId>PrologCafe</artifactId>
- <version>1.3</version>
- </dependency>
-
- <dependency>
- <groupId>org.pegdown</groupId>
- <artifactId>pegdown</artifactId>
- <version>1.1.0</version>
- </dependency>
-
- <dependency>
- <groupId>org.parboiled</groupId>
- <artifactId>parboiled-core</artifactId>
- <version>1.1.3</version>
- </dependency>
- <dependency>
- <groupId>org.parboiled</groupId>
- <artifactId>parboiled-java</artifactId>
- <version>1.1.3</version>
- </dependency>
- </dependencies>
- </dependencyManagement>
-
- <pluginRepositories>
- <pluginRepository>
- <id>gerrit-maven</id>
- <url>https://gerrit-maven.commondatastorage.googleapis.com</url>
- </pluginRepository>
- </pluginRepositories>
-
- <repositories>
- <repository>
- <id>gerrit-maven</id>
- <url>https://gerrit-maven.commondatastorage.googleapis.com</url>
- </repository>
-
- <repository>
- <id>jgit-repository</id>
- <url>http://download.eclipse.org/jgit/maven</url>
- </repository>
- </repositories>
-</project>
diff --git a/tools/BUCK b/tools/BUCK
new file mode 100644
index 0000000..4323ff9
--- /dev/null
+++ b/tools/BUCK
@@ -0,0 +1,53 @@
+genrule(
+ name = 'download',
+ cmd = '$(exe :download_all)',
+ deps = [':download_all'],
+ out = '__fake.download__',
+)
+
+genrule(
+ name = 'download_sources',
+ cmd = '$(exe :download_all) --src',
+ deps = [':download_all'],
+ out = '__fake.download__',
+)
+
+python_binary(
+ name = 'download_all',
+ main = 'download_all.py',
+)
+
+python_binary(
+ name = 'download_file',
+ main = 'download_file.py',
+ visibility = ['PUBLIC'],
+)
+
+python_binary(
+ name = 'pack_war',
+ main = 'pack_war.py',
+ deps = [':util'],
+ visibility = ['PUBLIC'],
+)
+
+python_library(
+ name = 'util',
+ srcs = ['util.py'],
+ visibility = ['PUBLIC'],
+)
+
+def shquote(s):
+ return s.replace("'", "'\\''")
+
+def os_path():
+ from os import environ
+ return environ.get('PATH')
+
+genrule(
+ name = 'buck.properties',
+ cmd = 'echo buck=`which buck`>$OUT;' +
+ ("echo PATH=\''%s'\' >>$OUT;" % shquote(os_path())),
+ deps = [],
+ out = 'buck.properties',
+ visibility = ['PUBLIC'],
+)
diff --git a/tools/build.defs b/tools/build.defs
new file mode 100644
index 0000000..053d529
--- /dev/null
+++ b/tools/build.defs
@@ -0,0 +1,80 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# These definitions support building a runnable version of Gerrit.
+
+DOCS = ['//Documentation:html.zip']
+LIBS = [
+ '//gerrit-war:log4j-config',
+ '//gerrit-war:init',
+ '//lib:postgresql',
+ '//lib/log:impl_log4j',
+]
+PGMLIBS = ['//gerrit-pgm:pgm']
+
+def scan_plugins():
+ import os
+ deps = []
+ for n in os.listdir('plugins'):
+ if os.path.exists(os.path.join('plugins', n, 'BUCK')):
+ deps.append('//plugins/%s:%s__plugin__compile' % (n, n))
+ return deps
+
+def war(
+ name,
+ libs = [],
+ pgmlibs = [],
+ context = [],
+ visibility = []
+ ):
+ cmd = ['$(exe //tools:pack_war)', '-o', '$OUT']
+ for l in libs:
+ cmd.extend(['--lib', l])
+ for l in pgmlibs:
+ cmd.extend(['--pgmlib', l])
+
+ src = []
+ dep = []
+ if context:
+ root = get_base_path()
+ if root:
+ root = '/'.join(['..' for _ in root.split('/')]) + '/'
+ for r in context:
+ dep.append(r[:r.rindex('.')])
+ if r.startswith('//'):
+ r = root + r[2:]
+ r = r.replace(':', '/')
+ src.append(genfile(r))
+ cmd.append('$SRCS')
+
+ genrule(
+ name = name,
+ cmd = ' '.join(cmd),
+ srcs = src,
+ deps = libs + pgmlibs + dep + ['//tools:pack_war'],
+ out = name + '.war',
+ visibility = visibility,
+ )
+
+def gerrit_war(name, ui = 'ui_optdbg', context = []):
+ war(
+ name = name,
+ libs = LIBS + ['//gerrit-war:version'],
+ pgmlibs = PGMLIBS,
+ context = [
+ '//gerrit-main:main_bin.jar',
+ '//gerrit-war:webapp_assets.zip',
+ '//gerrit-gwtui:' + ui + '.zip',
+ ] + context,
+ )
diff --git a/tools/default.defs b/tools/default.defs
new file mode 100644
index 0000000..e7981dd
--- /dev/null
+++ b/tools/default.defs
@@ -0,0 +1,178 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Rule definitions loaded by default into every BUCK file.
+
+def genantlr(
+ name,
+ srcs,
+ out):
+ tmp = name + '.src.zip'
+ genrule(
+ name = name,
+ srcs = srcs,
+ cmd = '$(exe //lib/antlr:antlr-tool) -o $TMP $SRCS;' +
+ 'cd $TMP;' +
+ 'zip -qr $OUT .',
+ deps = ['//lib/antlr:antlr-tool'],
+ out = out,
+ )
+
+def gwt_module(
+ name,
+ srcs,
+ gwtxml = None,
+ resources = [],
+ deps = [],
+ visibility = []):
+ if gwtxml:
+ resources = resources + [gwtxml]
+ resources = resources + srcs
+ java_library(
+ name = name,
+ srcs = srcs,
+ deps = deps,
+ resources = resources,
+ visibility = visibility,
+ )
+
+def gwt_application(
+ name,
+ module_target,
+ compiler_opts = [],
+ compiler_jvm_flags = [],
+ deps = [],
+ visibility = []):
+ cmd = ['$(exe //lib/gwt:compiler)', module_target, '$OUT']
+ cmd += compiler_opts + ['--', '$DEPS']
+ genrule(
+ name = name,
+ cmd = ' '.join(cmd),
+ deps = [
+ '//lib/gwt:compiler',
+ '//lib/gwt:dev',
+ ] + deps,
+ out = '%s.zip' % name,
+ visibility = visibility,
+ )
+
+# Compiles a Java library with additional compile-time dependencies
+# that do not show up as transitive dependencies to java_library()
+# or java_binary() rule that depends on this library.
+def java_library2(
+ name,
+ srcs = [],
+ resources = [],
+ deps = [],
+ compile_deps = [],
+ visibility = []):
+ c = name + '__compile'
+ t = name + '__link'
+ j = 'lib__%s__output/%s.jar' % (c, c)
+ o = 'lib__%s__output/%s.jar' % (name, name)
+ java_library(
+ name = c,
+ srcs = srcs,
+ resources = resources,
+ deps = deps + compile_deps,
+ visibility = ['//tools/eclipse:classpath'],
+ )
+ # Break the dependency chain by passing the newly built
+ # JAR to consumers through a prebuilt_jar().
+ genrule(
+ name = t,
+ cmd = 'mkdir -p $(dirname $OUT);ln -s $SRCS $OUT',
+ srcs = [genfile(j)],
+ deps = [':' + c],
+ out = o,
+ )
+ prebuilt_jar(
+ name = name,
+ binary_jar = genfile(o),
+ deps = deps + [':' + t],
+ visibility = visibility,
+ )
+
+def gerrit_extension(
+ name,
+ deps = [],
+ srcs = [],
+ resources = [],
+ manifest_file = None,
+ manifest_entries = [],
+ visibility = ['PUBLIC']):
+ gerrit_plugin(
+ name = name,
+ deps = deps,
+ srcs = srcs,
+ resources = resources,
+ manifest_file = manifest_file,
+ manifest_entries = manifest_entries,
+ type = 'extension',
+ visibility = visibility,
+ )
+
+def gerrit_plugin(
+ name,
+ deps = [],
+ srcs = [],
+ resources = [],
+ manifest_file = None,
+ manifest_entries = [],
+ type = 'plugin',
+ visibility = ['PUBLIC']):
+ mf_cmd = 'v=$(git describe HEAD);'
+ if manifest_file:
+ mf_src = [manifest_file]
+ mf_cmd += 'sed "s:@VERSION@:$v:g" $SRCS >$OUT'
+ else:
+ mf_src = []
+ mf_cmd += 'echo "Manifest-Version: 1.0" >$OUT;'
+ mf_cmd += 'echo "Gerrit-ApiType: %s" >>$OUT;' % type
+ mf_cmd += 'echo "Implementation-Version: $v" >>$OUT'
+ for line in manifest_entries:
+ mf_cmd += ';echo "%s" >> $OUT' % line
+ genrule(
+ name = name + '__manifest',
+ cmd = mf_cmd,
+ srcs = mf_src,
+ out = 'MANIFEST.MF',
+ )
+ java_library2(
+ name = name + '__plugin',
+ srcs = srcs,
+ resources = resources,
+ deps = deps,
+ compile_deps = ['//:%s-lib' % type],
+ )
+ java_binary(
+ name = name,
+ manifest_file = genfile('MANIFEST.MF'),
+ deps = [
+ ':%s__plugin' % name,
+ ':%s__manifest' % name,
+ ],
+ visibility = visibility,
+ )
+
+def java_sources(
+ name,
+ srcs,
+ visibility = []
+ ):
+ java_library(
+ name = name,
+ resources = srcs,
+ visibility = visibility,
+ )
diff --git a/tools/deploy_api.sh b/tools/deploy_api.sh
deleted file mode 100755
index 26baa31..0000000
--- a/tools/deploy_api.sh
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/bin/sh
-
-set -e
-
-SRC=$(ls gerrit-plugin-api/target/gerrit-plugin-api-*-sources.jar)
-VER=${SRC#gerrit-plugin-api/target/gerrit-plugin-api-}
-VER=${VER%-sources.jar}
-
-type=release
-case $VER in
-*-SNAPSHOT)
- echo >&2 "fatal: Cannot deploy $VER"
- echo >&2 " Use ./tools/version.sh --release && mvn clean package"
- exit 1
- ;;
-*-[0-9]*-g*) type=snapshot ;;
-esac
-URL=gs://gerrit-api/$type
-
-
-echo "Deploying $type gerrit-extension-api $VER"
-mvn deploy:deploy-file \
- -DgroupId=com.google.gerrit \
- -DartifactId=gerrit-extension-api \
- -Dversion=$VER \
- -Dpackaging=jar \
- -Dfile=gerrit-extension-api/target/gerrit-extension-api-$VER-all.jar \
- -DrepositoryId=gerrit-api-repository \
- -Durl=$URL
-
-mvn deploy:deploy-file \
- -DgroupId=com.google.gerrit \
- -DartifactId=gerrit-extension-api \
- -Dversion=$VER \
- -Dpackaging=java-source \
- -Dfile=gerrit-extension-api/target/gerrit-extension-api-$VER-all-sources.jar \
- -Djava-source=false \
- -DrepositoryId=gerrit-api-repository \
- -Durl=$URL
-
-
-echo "Deploying $type gerrit-plugin-api $VER"
-mvn deploy:deploy-file \
- -DgroupId=com.google.gerrit \
- -DartifactId=gerrit-plugin-api \
- -Dversion=$VER \
- -Dpackaging=jar \
- -Dfile=gerrit-plugin-api/target/gerrit-plugin-api-$VER.jar \
- -DrepositoryId=gerrit-api-repository \
- -Durl=$URL
-
-mvn deploy:deploy-file \
- -DgroupId=com.google.gerrit \
- -DartifactId=gerrit-plugin-api \
- -Dversion=$VER \
- -Dpackaging=java-source \
- -Dfile=gerrit-plugin-api/target/gerrit-plugin-api-$VER-sources.jar \
- -Djava-source=false \
- -DrepositoryId=gerrit-api-repository \
- -Durl=$URL
diff --git a/tools/download_all.py b/tools/download_all.py
new file mode 100755
index 0000000..241d20b
--- /dev/null
+++ b/tools/download_all.py
@@ -0,0 +1,44 @@
+#!/usr/bin/python
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from optparse import OptionParser
+import re
+from subprocess import check_call, CalledProcessError, Popen, PIPE
+
+MAIN = ['//tools/eclipse:classpath']
+PAT = re.compile(r'"(//.*?)" -> "//tools:download_file"')
+
+opts = OptionParser()
+opts.add_option('--src', action='store_true')
+args, _ = opts.parse_args()
+
+targets = set()
+
+p = Popen(['buck', 'audit', 'classpath', '--dot'] + MAIN, stdout = PIPE)
+for line in p.stdout:
+ m = PAT.search(line)
+ if m:
+ n = m.group(1)
+ if args.src and n.endswith('__download_bin'):
+ n = n[:-4] + '_src'
+ targets.add(n)
+r = p.wait()
+if r != 0:
+ exit(r)
+
+try:
+ check_call(['buck', 'build'] + sorted(targets))
+except CalledProcessError as err:
+ exit(1)
diff --git a/tools/download_file.py b/tools/download_file.py
new file mode 100755
index 0000000..4c6e19f
--- /dev/null
+++ b/tools/download_file.py
@@ -0,0 +1,191 @@
+#!/usr/bin/python
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import print_function
+
+from hashlib import sha1
+from optparse import OptionParser
+from os import link, makedirs, path, remove
+import shutil
+from subprocess import check_call, CalledProcessError
+from sys import stderr
+from zipfile import ZipFile, BadZipfile, LargeZipFile
+
+REPO_ROOTS = {
+ 'GERRIT': 'http://gerrit-maven.storage.googleapis.com',
+ 'MAVEN_CENTRAL': 'http://repo1.maven.org/maven2',
+ 'MAVEN_LOCAL': 'file://' + path.expanduser('~/.m2/repository'),
+}
+
+GERRIT_HOME = path.expanduser('~/.gerritcodereview')
+CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache')
+LOCAL_PROPERTIES = 'local.properties'
+
+
+def hashfile(p):
+ d = sha1()
+ with open(p, 'rb') as f:
+ while True:
+ b = f.read(8192)
+ if not b:
+ break
+ d.update(b)
+ return d.hexdigest()
+
+def safe_mkdirs(d):
+ if path.isdir(d):
+ return
+ try:
+ makedirs(d)
+ except OSError as err:
+ if not path.isdir(d):
+ raise err
+
+def download_properties(root_dir):
+ """ Get the download properties.
+
+ First tries to find the properties file in the given root directory,
+ and if not found there, tries in the Gerrit settings folder in the
+ user's home directory.
+
+ Returns a set of download properties, which may be empty.
+
+ """
+ p = {}
+ local_prop = path.join(root_dir, LOCAL_PROPERTIES)
+ if not path.isfile(local_prop):
+ local_prop = path.join(GERRIT_HOME, LOCAL_PROPERTIES)
+ if path.isfile(local_prop):
+ try:
+ with open(local_prop) as fd:
+ for line in fd:
+ if line.startswith('download.'):
+ d = [e.strip() for e in line.split('=', 1)]
+ name, url = d[0], d[1]
+ p[name[len('download.'):]] = url
+ except OSError:
+ pass
+ return p
+
+def cache_entry(args):
+ if args.v:
+ h = args.v
+ else:
+ h = sha1(args.u).hexdigest()
+ name = '%s-%s' % (path.basename(args.o), h)
+ return path.join(CACHE_DIR, name)
+
+def resolve_url(url, redirects):
+ s = url.find(':')
+ if s < 0:
+ return url
+ scheme, rest = url[:s], url[s+1:]
+ if scheme not in REPO_ROOTS:
+ return url
+ if scheme in redirects:
+ root = redirects[scheme]
+ else:
+ root = REPO_ROOTS[scheme]
+ root = root.rstrip('/')
+ rest = rest.lstrip('/')
+ return '/'.join([root, rest])
+
+opts = OptionParser()
+opts.add_option('-o', help='local output file')
+opts.add_option('-u', help='URL to download')
+opts.add_option('-v', help='expected content SHA-1')
+opts.add_option('-x', action='append', help='file to delete from ZIP')
+opts.add_option('--exclude_java_sources', action='store_true')
+args, _ = opts.parse_args()
+
+root_dir = args.o
+while root_dir:
+ root_dir, n = path.split(root_dir)
+ if n == 'buck-out':
+ break
+
+redirects = download_properties(root_dir)
+cache_ent = cache_entry(args)
+src_url = resolve_url(args.u, redirects)
+
+if not path.exists(cache_ent):
+ try:
+ safe_mkdirs(path.dirname(cache_ent))
+ except OSError as err:
+ print('error creating directory %s: %s' %
+ (path.dirname(cache_ent), err), file=stderr)
+ exit(1)
+
+ print('Download %s' % src_url, file=stderr)
+ try:
+ check_call(['curl', '--proxy-anyauth', '-sfo', cache_ent, src_url])
+ except OSError as err:
+ print('could not invoke curl: %s\nis curl installed?' % err, file=stderr)
+ exit(1)
+ except CalledProcessError as err:
+ print('error using curl: %s' % err, file=stderr)
+ exit(1)
+
+if args.v:
+ have = hashfile(cache_ent)
+ if args.v != have:
+ print((
+ '%s:\n' +
+ 'expected %s\n' +
+ 'received %s\n') % (src_url, args.v, have), file=stderr)
+ try:
+ remove(cache_ent)
+ except OSError as err:
+ if path.exists(cache_ent):
+ print('error removing %s: %s' % (cache_ent, err), file=stderr)
+ exit(1)
+
+exclude = []
+if args.x:
+ exclude += args.x
+if args.exclude_java_sources:
+ try:
+ zf = ZipFile(cache_ent, 'r')
+ try:
+ for n in zf.namelist():
+ if n.endswith('.java'):
+ exclude.append(n)
+ finally:
+ zf.close()
+ except (BadZipfile, LargeZipFile) as err:
+ print("error opening %s: %s" % (cache_ent, err), file=stderr)
+ exit(1)
+
+safe_mkdirs(path.dirname(args.o))
+if exclude:
+ try:
+ shutil.copyfile(cache_ent, args.o)
+ except (shutil.Error, IOError) as err:
+ print("error copying to %s: %s" % (args.o, err), file=stderr)
+ exit(1)
+ try:
+ check_call(['zip', '-d', args.o] + exclude)
+ except CalledProcessError as err:
+ print('error removing files from zip: %s' % err, file=stderr)
+ exit(1)
+else:
+ try:
+ link(cache_ent, args.o)
+ except OSError as err:
+ try:
+ shutil.copyfile(cache_ent, args.o)
+ except (shutil.Error, IOError) as err:
+ print("error copying to %s: %s" % (args.o, err), file=stderr)
+ exit(1)
diff --git a/tools/eclipse/BUCK b/tools/eclipse/BUCK
new file mode 100644
index 0000000..4e752e9
--- /dev/null
+++ b/tools/eclipse/BUCK
@@ -0,0 +1,71 @@
+include_defs('//tools/build.defs')
+
+genrule(
+ name = 'eclipse',
+ cmd = '',
+ deps = [
+ ':_classpath',
+ ':_project',
+ '//tools:buck.properties',
+ ],
+ out = '__fake.eclipse__',
+)
+
+genrule(
+ name = 'eclipse_project',
+ cmd = '',
+ deps = [
+ ':_classpath_nocompile',
+ ':_project',
+ '//tools:buck.properties',
+ ],
+ out = '__fake.eclipse__',
+)
+
+java_library(
+ name = 'classpath',
+ deps = LIBS + PGMLIBS + [
+ '//gerrit-acceptance-tests:lib',
+ '//gerrit-gwtdebug:gwtdebug',
+ '//gerrit-gwtui:ui_module',
+ '//gerrit-gwtui:ui_tests',
+ '//gerrit-httpd:httpd_tests',
+ '//gerrit-main:main_lib',
+ '//gerrit-server:server__compile',
+ '//lib/prolog:compiler_lib',
+ ] + scan_plugins(),
+)
+
+genrule(
+ name = '_project',
+ cmd = '$(exe :gen_project)',
+ deps = [':gen_project'],
+ out = '__fake.project__',
+)
+
+genrule(
+ name = '_classpath',
+ cmd = '$(exe :gen_classpath)',
+ deps = [
+ ':classpath',
+ ':gen_classpath',
+ ],
+ out = '__fake.classpath__',
+)
+
+genrule(
+ name = '_classpath_nocompile',
+ cmd = '$(exe :gen_classpath)',
+ deps = [':gen_classpath'],
+ out = '__fake.classpath__',
+)
+
+python_binary(
+ name = 'gen_classpath',
+ main = 'gen_classpath.py',
+)
+
+python_binary(
+ name = 'gen_project',
+ main = 'gen_project.py',
+)
diff --git a/tools/eclipse/buck_daemon_ui_chrome.launch b/tools/eclipse/buck_daemon_ui_chrome.launch
new file mode 100644
index 0000000..efe2623
--- /dev/null
+++ b/tools/eclipse/buck_daemon_ui_chrome.launch
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/gerrit/gerrit-main/src/main/java/Main.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
+<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../test_site"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dgerrit.browser=ui_chrome"/>
+</launchConfiguration>
diff --git a/tools/eclipse/buck_daemon_ui_dbg.launch b/tools/eclipse/buck_daemon_ui_dbg.launch
new file mode 100644
index 0000000..a345f8a
--- /dev/null
+++ b/tools/eclipse/buck_daemon_ui_dbg.launch
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/gerrit/gerrit-main/src/main/java/Main.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../test_site"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dgerrit.browser=ui_dbg"/>
+</launchConfiguration>
diff --git a/tools/eclipse/buck_daemon_ui_firefox.launch b/tools/eclipse/buck_daemon_ui_firefox.launch
new file mode 100644
index 0000000..383b051
--- /dev/null
+++ b/tools/eclipse/buck_daemon_ui_firefox.launch
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/gerrit/gerrit-main/src/main/java/Main.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../test_site"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dgerrit.browser=ui_firefox"/>
+</launchConfiguration>
diff --git a/tools/eclipse/buck_daemon_ui_ie9.launch b/tools/eclipse/buck_daemon_ui_ie9.launch
new file mode 100644
index 0000000..18863e7
--- /dev/null
+++ b/tools/eclipse/buck_daemon_ui_ie9.launch
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/gerrit/gerrit-main/src/main/java/Main.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../test_site"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dgerrit.browser=ui_ie9"/>
+</launchConfiguration>
diff --git a/tools/eclipse/buck_daemon_ui_safari.launch b/tools/eclipse/buck_daemon_ui_safari.launch
new file mode 100644
index 0000000..55259a9
--- /dev/null
+++ b/tools/eclipse/buck_daemon_ui_safari.launch
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/gerrit/gerrit-main/src/main/java/Main.java"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon --console-log --show-stack-trace -d ${resource_loc:/gerrit}/../test_site"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Dgerrit.browser=ui_safari"/>
+</launchConfiguration>
diff --git a/tools/eclipse/buck_gwt_debug.launch b/tools/eclipse/buck_gwt_debug.launch
new file mode 100644
index 0000000..1723cbf
--- /dev/null
+++ b/tools/eclipse/buck_gwt_debug.launch
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/gerrit/buck-out/gen/lib/gwt/dev/gwt-dev-2.5.0.jar"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="1"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
+<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gwt.dev.DevMode"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-startupUrl / -war ${resource_loc:/gerrit}/buck-out/gen/gerrit-gwtui/ui_dbg__tmp/war -server com.google.gerrit.gwtdebug.GerritDebugLauncher com.google.gerrit.GerritGwtUI"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M -XX:MaxPermSize=128M -Dgwt.persistentunitcachedir=${resource_loc:/gerrit}/buck-out/gen/gerrit-gwtui/ui_dbg__tmp/unit_cache -Dgerrit.source_root=${resource_loc:/gerrit} -Dgerrit.site_path=${resource_loc:/gerrit}/../test_site -da:com.google.gwtexpui.globalkey.client.KeyCommandSet"/>
+</launchConfiguration>
diff --git a/tools/eclipse/gen_classpath.py b/tools/eclipse/gen_classpath.py
new file mode 100755
index 0000000..241a876
--- /dev/null
+++ b/tools/eclipse/gen_classpath.py
@@ -0,0 +1,118 @@
+#!/usr/bin/python
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# TODO(sop): Remove hack after Buck supports Eclipse
+
+from os import path
+import re
+from subprocess import Popen, PIPE
+from sys import argv
+from xml.dom import minidom
+
+ROOT = path.abspath(__file__)
+for _ in range(0, 3):
+ ROOT = path.dirname(ROOT)
+
+MAIN = ['//tools/eclipse:classpath']
+GWT = ['//gerrit-gwtui:ui_module']
+JRE = '/'.join([
+ 'org.eclipse.jdt.launching.JRE_CONTAINER',
+ 'org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType',
+ 'JavaSE-1.6',
+])
+
+def query_classpath(targets):
+ deps = []
+ p = Popen(['buck', 'audit', 'classpath'] + targets, stdout = PIPE)
+ for line in p.stdout:
+ deps.append(line.strip())
+ s = p.wait()
+ if s != 0:
+ exit(s)
+ return deps
+
+def make_classpath():
+ impl = minidom.getDOMImplementation()
+ return impl.createDocument(None, 'classpath', None)
+
+doc = make_classpath()
+src = set()
+lib = set()
+gwt_src = set()
+gwt_lib = set()
+
+def classpathentry(kind, path, src = None):
+ e = doc.createElement('classpathentry')
+ e.setAttribute('kind', kind)
+ e.setAttribute('path', path)
+ if src:
+ e.setAttribute('sourcepath', src)
+ doc.documentElement.appendChild(e)
+
+java_library = re.compile(r'[^/]+/gen/(.*)/lib__[^/]+__output/[^/]+[.]jar$')
+for p in query_classpath(MAIN):
+ if p.endswith('-src.jar'):
+ # gwt_module() depends on -src.jar for Java to JavaScript compiles.
+ gwt_lib.add(p)
+ continue
+
+ if p.startswith('buck-out/gen/lib/gwt/'):
+ # gwt_module() depends on huge shaded GWT JARs that import
+ # incorrect versions of classes for Gerrit. Collect into
+ # a private grouping for later use.
+ gwt_lib.add(p)
+ continue
+
+ m = java_library.match(p)
+ if m:
+ src.add(m.group(1))
+ else:
+ lib.add(p)
+
+for p in query_classpath(GWT):
+ m = java_library.match(p)
+ if m:
+ gwt_src.add(m.group(1))
+
+for s in sorted(src):
+ p = path.join(s, 'java')
+ if path.exists(p):
+ classpathentry('src', p)
+ continue
+
+ for env in ['main', 'test']:
+ for type in ['java', 'resources']:
+ p = path.join(s, 'src', env, type)
+ if path.exists(p):
+ classpathentry('src', p)
+
+for libs in [lib, gwt_lib]:
+ for j in sorted(libs):
+ s = None
+ if j.endswith('.jar'):
+ s = j[:-4] + '-src.jar'
+ if not path.exists(s):
+ s = None
+ classpathentry('lib', j, s)
+
+for s in sorted(gwt_src):
+ classpathentry('lib', path.join(ROOT, s, 'src', 'main', 'java'))
+
+classpathentry('con', JRE)
+classpathentry('output', 'buck-out/classes')
+
+p = path.join(ROOT, '.classpath')
+with open(p, 'w') as fd:
+ doc.writexml(fd, addindent = ' ', newl = '\n', encoding='UTF-8')
diff --git a/tools/eclipse/gen_project.py b/tools/eclipse/gen_project.py
new file mode 100755
index 0000000..d42d43c
--- /dev/null
+++ b/tools/eclipse/gen_project.py
@@ -0,0 +1,42 @@
+#!/usr/bin/python
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# TODO(sop): Remove hack after Buck supports Eclipse
+
+from __future__ import print_function
+
+from os import path
+from sys import argv
+
+ROOT = path.abspath(__file__)
+for _ in range(0, 3):
+ ROOT = path.dirname(ROOT)
+
+p = path.join(ROOT, '.project')
+with open(p, 'w') as fd:
+ print("""\
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>gerrit</name>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>\
+""", file=fd)
diff --git a/tools/git.defs b/tools/git.defs
new file mode 100644
index 0000000..c58ac88
--- /dev/null
+++ b/tools/git.defs
@@ -0,0 +1,23 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+def git_describe():
+ import subprocess
+ cmd = ['git', 'describe', '--match', 'v[0-9].*', '--dirty']
+ p = subprocess.Popen(cmd, stdout = subprocess.PIPE)
+ v = p.communicate()[0].strip()
+ r = p.returncode
+ if r != 0:
+ raise subprocess.CalledProcessError(r, ' '.join(cmd))
+ return v
diff --git a/tools/gwtui_dbg.launch b/tools/gwtui_dbg.launch
deleted file mode 100644
index 8a873be..0000000
--- a/tools/gwtui_dbg.launch
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
-<stringAttribute key="bad_container_name" value="/gerrit-appja"/>
-<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
-<listEntry value="/gerrit-gwtdebug"/>
-</listAttribute>
-<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
-<listEntry value="4"/>
-</listAttribute>
-<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
-<stringAttribute key="org.eclipse.debug.core.source_locator_id" value="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"/>
-<stringAttribute key="org.eclipse.debug.core.source_locator_memento" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <sourceLookupDirector> <sourceContainers duplicates="false"> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-common&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-httpd&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-patch-commonsnet&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-prettify&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-patch-jgit&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-pgm&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-server&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-sshd&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-util-cli&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-util-ssl&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-reviewdb&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-gwtui&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-gwtdebug&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;default/&gt;&#10;" typeId="org.eclipse.debug.core.containerType.default"/> </sourceContainers> </sourceLookupDirector> "/>
-<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
-<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
-<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
-</listAttribute>
-<booleanAttribute key="org.eclipse.jdt.debug.ui.CONSIDER_INHERITED_MAIN" value="true"/>
-<listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
-<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry containerPath="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6" path="1" type="4"/> "/>
-<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry path="3" projectName="gerrit-gwtdebug" type="1"/> "/>
-<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry path="3" projectName="gerrit-war" type="1"/> "/>
-<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/gerrit-prettify/src/main/java" path="3" type="2"/> "/>
-<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/gerrit-patch-jgit/src/main/java" path="3" type="2"/> "/>
-<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/gerrit-reviewdb/src/main/java" path="3" type="2"/> "/>
-<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/gerrit-common/src/main/java" path="3" type="2"/> "/>
-<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/gerrit-gwtui/src/main/java" path="3" type="2"/> "/>
-<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry containerPath="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER" path="3" type="4"/> "/>
-<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/gerrit-gwtexpui/src/main/java" path="3" type="2"/> "/>
-<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/gwtjsonrpc/src/main/java" path="3" type="2"/> "/>
-<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/gwtorm/src/main/java" path="3" type="2"/> "/>
-<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/gerrit-gwtui/target/classes" path="3" type="2"/> "/>
-</listAttribute>
-<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.m2e.launchconfig.classpathProvider"/>
-<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
-<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gwt.dev.DevMode"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-startupUrl / -war ${resource_loc:/gerrit-gwtui/target}/gwt-hosted-mode -server com.google.gerrit.gwtdebug.GerritDebugLauncher com.google.gerrit.GerritGwtUI"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit-gwtdebug"/>
-<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.m2e.launchconfig.sourcepathProvider"/>
-<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M -da:com.google.gwtexpui.globalkey.client.KeyCommandSet -Dgerrit.site_path=${resource_loc:/gerrit-parent}/../test_site"/>
-</launchConfiguration>
diff --git a/tools/maven/BUCK b/tools/maven/BUCK
new file mode 100644
index 0000000..0a470a4
--- /dev/null
+++ b/tools/maven/BUCK
@@ -0,0 +1,24 @@
+include_defs('//VERSION')
+include_defs('//tools/maven/package.defs')
+
+TYPE = 'snapshot' if GERRIT_VERSION.endswith('-SNAPSHOT') else 'release'
+
+maven_package(
+ repository = 'gerrit-api-repository',
+ url = 's3://gerrit-api@commondatastorage.googleapis.com/%s' % TYPE,
+ version = GERRIT_VERSION,
+ jar = {
+ 'gerrit-extension-api': '//:extension-api',
+ 'gerrit-plugin-api': '//:plugin-api',
+ },
+ src = {
+ 'gerrit-extension-api': '//:extension-api-src',
+ 'gerrit-plugin-api': '//:plugin-api-src',
+ },
+)
+
+python_binary(
+ name = 'mvn',
+ main = 'mvn.py',
+ deps = ['//tools:util'],
+)
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py
new file mode 100644
index 0000000..dc098a3
--- /dev/null
+++ b/tools/maven/mvn.py
@@ -0,0 +1,61 @@
+#!/usr/bin/python
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import print_function
+from optparse import OptionParser
+from sys import stderr
+from util import check_output
+
+opts = OptionParser()
+opts.add_option('--repository', help='maven repository id')
+opts.add_option('--url', help='maven repository url')
+opts.add_option('-a', help='action (valid actions are: install,deploy)')
+opts.add_option('-v', help='gerrit version')
+opts.add_option('-s', action='append', help='triplet of artifactId:type:path')
+
+args, ctx = opts.parse_args()
+if not args.v:
+ print('version is empty', file=stderr)
+ exit(1)
+
+common = [
+ '-DgroupId=com.google.gerrit',
+ '-Dversion=%s' % args.v,
+]
+
+if 'install' == args.a:
+ cmd = ['mvn', 'install:install-file'] + common
+elif 'deploy' == args.a:
+ cmd = [
+ 'mvn',
+ 'deploy:deploy-file',
+ '-DrepositoryId=%s' % args.repository,
+ '-Durl=%s' % args.url,
+ ] + common
+else:
+ print("unknown action -a %s" % args.a, file=stderr)
+ exit(1)
+
+for spec in args.s:
+ artifact, type, src = spec.split(':')
+ try:
+ check_output(cmd + [
+ '-DartifactId=%s' % artifact,
+ '-Dpackaging=%s' % type,
+ '-Dfile=%s' % src,
+ ])
+ except Exception as e:
+ print('%s command failed: %s' % (args.a, e), file=stderr)
+ exit(1)
diff --git a/tools/maven/package.defs b/tools/maven/package.defs
new file mode 100644
index 0000000..b19701f
--- /dev/null
+++ b/tools/maven/package.defs
@@ -0,0 +1,45 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+def maven_package(
+ version,
+ repository = None,
+ url = None,
+ jar = {},
+ src = {}):
+ cmd = ['$(exe //tools/maven:mvn)', '-v', version]
+ dep = []
+
+ for type,d in [('jar', jar), ('java-source', src)]:
+ for a,t in d.iteritems():
+ cmd.append('-s %s:%s:$(location %s)' % (a,type,t))
+ dep.append(t)
+
+ genrule(
+ name = 'install',
+ cmd = ' '.join(cmd + ['-a', 'install']),
+ deps = dep + ['//tools/maven:mvn'],
+ out = '__fake.install__',
+ )
+
+ if repository and url:
+ genrule(
+ name = 'deploy',
+ cmd = ' '.join(cmd + [
+ '-a', 'deploy',
+ '--repository', repository,
+ '--url', url]),
+ deps = dep + ['//tools/maven:mvn'],
+ out = '__fake.deploy__',
+ )
diff --git a/tools/pack_war.py b/tools/pack_war.py
new file mode 100755
index 0000000..0ba802b
--- /dev/null
+++ b/tools/pack_war.py
@@ -0,0 +1,48 @@
+#!/usr/bin/python
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from optparse import OptionParser
+from os import environ, makedirs, path, symlink
+from subprocess import check_call
+from util import check_output
+
+opts = OptionParser()
+opts.add_option('-o', help='path to write WAR to')
+opts.add_option('--lib', action='append', help='target for WEB-INF/lib')
+opts.add_option('--pgmlib', action='append', help='target for WEB-INF/pgm-lib')
+args, ctx = opts.parse_args()
+
+war = environ['TMP']
+root = war[:war.index('buck-out')]
+jars = set()
+
+def link_jars(libs, dir):
+ makedirs(dir)
+ cp = check_output(['buck', 'audit', 'classpath'] + libs)
+ for j in cp.strip().splitlines():
+ if j not in jars:
+ jars.add(j)
+ n = path.basename(j)
+ if j.startswith('buck-out/gen/gerrit-'):
+ n = j.split('/')[2] + '-' + n
+ symlink(path.join(root, j), path.join(dir, n))
+
+if args.lib:
+ link_jars(args.lib, path.join(war, 'WEB-INF', 'lib'))
+if args.pgmlib:
+ link_jars(args.pgmlib, path.join(war, 'WEB-INF', 'pgm-lib'))
+for s in ctx:
+ check_call(['unzip', '-q', '-d', war, s])
+check_call(['zip', '-9qr', args.o, '.'], cwd = war)
diff --git a/tools/pgm_daemon.launch b/tools/pgm_daemon.launch
deleted file mode 100644
index cedf470..0000000
--- a/tools/pgm_daemon.launch
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
-<stringAttribute key="bad_container_name" value="/gerrit-appja"/>
-<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
-<listEntry value="/gerrit-main/src/main/java/Main.java"/>
-</listAttribute>
-<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
-<listEntry value="1"/>
-</listAttribute>
-<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
-<stringAttribute key="org.eclipse.debug.core.source_locator_id" value="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"/>
-<stringAttribute key="org.eclipse.debug.core.source_locator_memento" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <sourceLookupDirector> <sourceContainers duplicates="false"> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-common&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-httpd&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-patch-commonsnet&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-patch-jgit&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-pgm&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-server&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-sshd&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-util-cli&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-util-ssl&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-war&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-reviewdb&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;javaProject name=&quot;gerrit-main&quot;/&gt;&#10;" typeId="org.eclipse.jdt.launching.sourceContainer.javaProject"/> <container memento="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;default/&gt;&#10;" typeId="org.eclipse.debug.core.containerType.default"/> </sourceContainers> </sourceLookupDirector> "/>
-<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
-<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
-<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
-</listAttribute>
-<booleanAttribute key="org.eclipse.jdt.debug.ui.CONSIDER_INHERITED_MAIN" value="true"/>
-<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.m2e.launchconfig.classpathProvider"/>
-<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon --console-log --show-stack-trace -d ${resource_loc:/gerrit-parent}/../test_site"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit-war"/>
-<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.m2e.launchconfig.sourcepathProvider"/>
-<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M"/>
-</launchConfiguration>
diff --git a/tools/release.sh b/tools/release.sh
deleted file mode 100755
index b8f3156..0000000
--- a/tools/release.sh
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/bin/sh
-
-flags=
-
-while [ $# -gt 0 ]
-do
- case "$1" in
- --no-documentation|--without-documentation)
- flags="$flags -Dgerrit.documentation.skip=true"
- shift
- ;;
- --no-plugins|--without-plugins)
- flags="$flags -Dgerrit.plugins.skip=true"
- shift
- ;;
- --no-tests|--without-tests)
- flags="$flags -Dgerrit.acceptance-tests.skip=true"
- flags="$flags -Dmaven.test.skip=true"
- shift
- ;;
- *)
- echo >&2 "usage: $0 [--no-documentation] [--no-plugins] [--no-tests]"
- exit 1
- esac
-done
-
-git update-index -q --refresh
-
-if test -n "$(git diff-index --name-only HEAD --)" \
-|| test -n "$(git ls-files --others --exclude-standard)"
-then
- echo >&2 "error: working directory is dirty, refusing to build"
- exit 1
-fi
-
-./tools/version.sh --release &&
-mvn clean package verify $flags
-rc=$?
-./tools/version.sh --reset
-
-if test 0 = $rc
-then
- echo
- echo Built Gerrit Code Review `git describe`:
- ls gerrit-war/target/gerrit-*.war
- echo
-fi
-exit $rc
diff --git a/tools/util.py b/tools/util.py
new file mode 100644
index 0000000..0c121e1
--- /dev/null
+++ b/tools/util.py
@@ -0,0 +1,20 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+try:
+ from subprocess import check_output
+except ImportError:
+ from subprocess import Popen, PIPE
+ def check_output(*cmd):
+ return Popen(*cmd, stdout=PIPE).communicate()[0]
diff --git a/tools/version.sh b/tools/version.sh
deleted file mode 100755
index def099d..0000000
--- a/tools/version.sh
+++ /dev/null
@@ -1,69 +0,0 @@
-#!/bin/sh
-
-# Update all pom.xml with new build number
-#
-# TODO(sop) This should be converted to some sort of
-# Java based Maven plugin so its fully portable.
-#
-
-SERVER_POMS=$(git ls-files | grep pom.xml | grep -v /src/main/resources/archetype-resources/pom.xml)
-POM_FILES=$SERVER_POMS
-
-# CORE PLUGIN LIST
-PLUGINS="commit-message-length-validator replication reviewnotes"
-for p in $PLUGINS
-do
- POM_FILES="$POM_FILES $(cd plugins/$p && git ls-files | grep pom.xml | sed s,^,plugins/$p/,)"
-done
-
-case "$1" in
---snapshot=*)
- V=$(echo "$1" | perl -pe 's/^--snapshot=//')
- if [ -z "$V" ]
- then
- echo >&2 "usage: $0 --snapshot=0.n.0"
- exit 1
- fi
- case "$V" in
- *-SNAPSHOT) : ;;
- *) V=$V-SNAPSHOT ;;
- esac
- ;;
-
---release)
- V=$(git describe HEAD) || exit
- ;;
-
---reset)
- git checkout HEAD -- $SERVER_POMS
- for p in $PLUGINS
- do
- (cd plugins/$p; git checkout $(git ls-files | grep pom.xml))
- done
- exit $?
- ;;
-
-*)
- echo >&2 "usage: $0 {--snapshot=2.n | --release}"
- exit 1
-esac
-
-case "$V" in
-v*) V=$(echo "$V" | perl -pe s/^v//) ;;
-esac
-
-perl -pi.bak -e '
- if ($ARGV ne $old_argv) {
- $seen_version = 0;
- $old_argv = $ARGV;
- }
- if (!$seen_version) {
- $seen_version = 1 if
- s{(<version>).*(</version>)}{${1}'"$V"'${2}};
- }
- ' $POM_FILES
-
-for pom in $POM_FILES
-do
- rm -f ${pom}.bak
-done
diff --git a/website/releases/index.html b/website/releases/index.html
new file mode 100644
index 0000000..0ca0cb4
--- /dev/null
+++ b/website/releases/index.html
@@ -0,0 +1,155 @@
+<html>
+<head>
+ <title>Gerrit Code Review - Releases</title>
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.2/jquery.min.js"></script>
+ <style>
+ #diffy_logo {
+ float: left;
+ width: 75px;
+ height: 70px;
+ margin-right: 20px;
+ }
+ #download_container table {
+ border-spacing: 0;
+ }
+ #download_container td {
+ padding-right: 5px;
+ }
+ .latest-release {
+ background-color: lightgreen;
+ }
+ .rc {
+ padding-left: 1em;
+ font-style: italic;
+ }
+ .size {
+ text-align: right;
+ }
+ </style>
+</head>
+<body>
+
+<h1>Gerrit Code Review - Releases</h1>
+<a href="http://code.google.com/p/gerrit">
+ <img id="diffy_logo" src="https://gerrit-review.googlesource.com/static/diffy1.cache.png">
+</a>
+
+<div id='download_container'>
+</div>
+
+<script>
+$.getJSON(
+'https://www.googleapis.com/storage/v1beta2/b/gerrit-releases/o?projection=noAcl&fields=items(name%2Csize)&callback=?',
+function(data) {
+ var doc = document;
+ var frg = doc.createDocumentFragment();
+ var rx = /^gerrit(?:-full)?-([0-9.]+(?:-rc[0-9]+)?)[.]war/;
+ var rel = 'http://gerrit-documentation.googlecode.com/svn/ReleaseNotes/';
+ var src = 'https://gerrit.googlesource.com/gerrit/+/'
+
+ data.items.sort(function(a,b) {
+ var av = rx.exec(a.name);
+ var bv = rx.exec(b.name);
+ if (!av || !bv) {
+ return a.name > b.name ? 1 : -1;
+ }
+
+ var an = av[1].replace('-rc', '.rc').split('.')
+ var bn = bv[1].replace('-rc', '.rc').split('.')
+ while (an.length < bn.length) an.push('0');
+ while (an.length > bn.length) bn.push('0');
+ for (var i = 0; i < an.length; i++) {
+ var ai = an[i].indexOf('rc') == 0
+ ? parseInt(an[i].substring(2))
+ : 1000 + parseInt(an[i]);
+
+ var bi = bn[i].indexOf('rc') == 0
+ ? parseInt(bn[i].substring(2))
+ : 1000 + parseInt(bn[i]);
+
+ if (ai != bi) {
+ return ai > bi ? -1 : 1;
+ }
+ }
+ return 0;
+ });
+
+ var latest = false;
+ for (var i = 0; i < data.items.length; i++) {
+ var f = data.items[i];
+ var v = rx.exec(f.name);
+
+ if ('index.html' == f.name) {
+ continue;
+ }
+
+ var tr = doc.createElement('tr');
+ var td = doc.createElement('td');
+ var a = doc.createElement('a');
+ a.href = f.name;
+ if (v) {
+ a.appendChild(doc.createTextNode('Gerrit ' + v[1]));
+ } else {
+ a.appendChild(doc.createTextNode(f.name));
+ }
+ if (f.name.indexOf('-rc') > 0) {
+ td.className = 'rc';
+ } else if (!latest) {
+ latest = true;
+ tr.className='latest-release';
+ }
+ td.appendChild(a);
+ tr.appendChild(td);
+
+ td = doc.createElement('td');
+ td.className = 'size';
+ if (f.size/(1024*1024) < 1) {
+ sizeText = Math.round(f.size/1024*10)/10 + ' KiB';
+ } else {
+ sizeText = Math.round(f.size/(1024*1024)*10)/10 + ' MiB';
+ }
+ td.appendChild(doc.createTextNode(sizeText));
+ tr.appendChild(td);
+
+ td = doc.createElement('td');
+ if (v && f.name.indexOf('-rc') < 0) {
+ a = doc.createElement('a');
+ a.href = rel + 'ReleaseNotes-' + v[1] + '.html';
+ a.appendChild(doc.createTextNode('Release Notes'));
+ td.appendChild(a);
+ }
+ tr.appendChild(td);
+
+ td = doc.createElement('td');
+ if (v) {
+ a = doc.createElement('a');
+ a.href = src + 'v' + v[1];
+ a.appendChild(doc.createTextNode('src'));
+ td.appendChild(a);
+ }
+ tr.appendChild(td);
+
+ frg.appendChild(tr);
+ }
+
+ var tr = doc.createElement('tr');
+ var th = doc.createElement('th');
+ th.appendChild(doc.createTextNode('File'));
+ tr.appendChild(th);
+
+ th = doc.createElement('th');
+ th.appendChild(doc.createTextNode('Size'));
+ tr.appendChild(th);
+
+ tr.appendChild(doc.createElement('th'));
+ tr.appendChild(doc.createElement('th'));
+
+ var table = doc.createElement('table');
+ table.appendChild(tr);
+ table.appendChild(frg);
+ doc.getElementById('download_container').appendChild(table);
+});
+</script>
+
+</body>
+</html>