diff options
Diffstat (limited to 'org.eclipse.jgit')
306 files changed, 68444 insertions, 0 deletions
diff --git a/org.eclipse.jgit/.classpath b/org.eclipse.jgit/.classpath new file mode 100644 index 0000000000..304e86186a --- /dev/null +++ b/org.eclipse.jgit/.classpath @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"/> + <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/org.eclipse.jgit/.fbprefs b/org.eclipse.jgit/.fbprefs new file mode 100644 index 0000000000..81a0767ff6 --- /dev/null +++ b/org.eclipse.jgit/.fbprefs @@ -0,0 +1,125 @@ +#FindBugs User Preferences +#Mon May 04 16:24:13 PDT 2009 +detectorAppendingToAnObjectOutputStream=AppendingToAnObjectOutputStream|true +detectorBadAppletConstructor=BadAppletConstructor|false +detectorBadResultSetAccess=BadResultSetAccess|true +detectorBadSyntaxForRegularExpression=BadSyntaxForRegularExpression|true +detectorBadUseOfReturnValue=BadUseOfReturnValue|true +detectorBadlyOverriddenAdapter=BadlyOverriddenAdapter|true +detectorBooleanReturnNull=BooleanReturnNull|true +detectorCallToUnsupportedMethod=CallToUnsupportedMethod|true +detectorCheckImmutableAnnotation=CheckImmutableAnnotation|true +detectorCheckTypeQualifiers=CheckTypeQualifiers|true +detectorCloneIdiom=CloneIdiom|false +detectorComparatorIdiom=ComparatorIdiom|true +detectorConfusedInheritance=ConfusedInheritance|true +detectorConfusionBetweenInheritedAndOuterMethod=ConfusionBetweenInheritedAndOuterMethod|true +detectorCrossSiteScripting=CrossSiteScripting|true +detectorDoInsideDoPrivileged=DoInsideDoPrivileged|true +detectorDontCatchIllegalMonitorStateException=DontCatchIllegalMonitorStateException|true +detectorDontUseEnum=DontUseEnum|true +detectorDroppedException=DroppedException|true +detectorDumbMethodInvocations=DumbMethodInvocations|true +detectorDumbMethods=DumbMethods|true +detectorDuplicateBranches=DuplicateBranches|true +detectorEmptyZipFileEntry=EmptyZipFileEntry|true +detectorEqualsOperandShouldHaveClassCompatibleWithThis=EqualsOperandShouldHaveClassCompatibleWithThis|true +detectorFinalizerNullsFields=FinalizerNullsFields|true +detectorFindBadCast2=FindBadCast2|true +detectorFindBadForLoop=FindBadForLoop|true +detectorFindCircularDependencies=FindCircularDependencies|false +detectorFindDeadLocalStores=FindDeadLocalStores|true +detectorFindDoubleCheck=FindDoubleCheck|true +detectorFindEmptySynchronizedBlock=FindEmptySynchronizedBlock|true +detectorFindFieldSelfAssignment=FindFieldSelfAssignment|true +detectorFindFinalizeInvocations=FindFinalizeInvocations|true +detectorFindFloatEquality=FindFloatEquality|true +detectorFindHEmismatch=FindHEmismatch|true +detectorFindInconsistentSync2=FindInconsistentSync2|true +detectorFindJSR166LockMonitorenter=FindJSR166LockMonitorenter|true +detectorFindLocalSelfAssignment2=FindLocalSelfAssignment2|true +detectorFindMaskedFields=FindMaskedFields|true +detectorFindMismatchedWaitOrNotify=FindMismatchedWaitOrNotify|true +detectorFindNakedNotify=FindNakedNotify|true +detectorFindNonSerializableStoreIntoSession=FindNonSerializableStoreIntoSession|true +detectorFindNonSerializableValuePassedToWriteObject=FindNonSerializableValuePassedToWriteObject|true +detectorFindNonShortCircuit=FindNonShortCircuit|true +detectorFindNullDeref=FindNullDeref|true +detectorFindNullDerefsInvolvingNonShortCircuitEvaluation=FindNullDerefsInvolvingNonShortCircuitEvaluation|true +detectorFindOpenStream=FindOpenStream|true +detectorFindPuzzlers=FindPuzzlers|true +detectorFindRefComparison=FindRefComparison|true +detectorFindReturnRef=FindReturnRef|true +detectorFindRunInvocations=FindRunInvocations|true +detectorFindSelfComparison=FindSelfComparison|true +detectorFindSelfComparison2=FindSelfComparison2|true +detectorFindSleepWithLockHeld=FindSleepWithLockHeld|true +detectorFindSpinLoop=FindSpinLoop|true +detectorFindSqlInjection=FindSqlInjection|true +detectorFindTwoLockWait=FindTwoLockWait|true +detectorFindUncalledPrivateMethods=FindUncalledPrivateMethods|true +detectorFindUnconditionalWait=FindUnconditionalWait|true +detectorFindUninitializedGet=FindUninitializedGet|true +detectorFindUnrelatedTypesInGenericContainer=FindUnrelatedTypesInGenericContainer|true +detectorFindUnreleasedLock=FindUnreleasedLock|true +detectorFindUnsatisfiedObligation=FindUnsatisfiedObligation|true +detectorFindUnsyncGet=FindUnsyncGet|true +detectorFindUselessControlFlow=FindUselessControlFlow|true +detectorFormatStringChecker=FormatStringChecker|true +detectorHugeSharedStringConstants=HugeSharedStringConstants|true +detectorIDivResultCastToDouble=IDivResultCastToDouble|true +detectorIncompatMask=IncompatMask|true +detectorInconsistentAnnotations=InconsistentAnnotations|true +detectorInefficientMemberAccess=InefficientMemberAccess|false +detectorInefficientToArray=InefficientToArray|true +detectorInfiniteLoop=InfiniteLoop|true +detectorInfiniteRecursiveLoop=InfiniteRecursiveLoop|true +detectorInfiniteRecursiveLoop2=InfiniteRecursiveLoop2|false +detectorInheritanceUnsafeGetResource=InheritanceUnsafeGetResource|true +detectorInitializationChain=InitializationChain|true +detectorInstantiateStaticClass=InstantiateStaticClass|true +detectorInvalidJUnitTest=InvalidJUnitTest|true +detectorIteratorIdioms=IteratorIdioms|true +detectorLazyInit=LazyInit|true +detectorLoadOfKnownNullValue=LoadOfKnownNullValue|true +detectorMethodReturnCheck=MethodReturnCheck|true +detectorMultithreadedInstanceAccess=MultithreadedInstanceAccess|true +detectorMutableLock=MutableLock|true +detectorMutableStaticFields=MutableStaticFields|true +detectorNaming=Naming|true +detectorNumberConstructor=NumberConstructor|true +detectorOverridingEqualsNotSymmetrical=OverridingEqualsNotSymmetrical|true +detectorPreferZeroLengthArrays=PreferZeroLengthArrays|true +detectorPublicSemaphores=PublicSemaphores|false +detectorQuestionableBooleanAssignment=QuestionableBooleanAssignment|true +detectorReadReturnShouldBeChecked=ReadReturnShouldBeChecked|true +detectorRedundantInterfaces=RedundantInterfaces|true +detectorRepeatedConditionals=RepeatedConditionals|true +detectorRuntimeExceptionCapture=RuntimeExceptionCapture|true +detectorSerializableIdiom=SerializableIdiom|true +detectorStartInConstructor=StartInConstructor|true +detectorStaticCalendarDetector=StaticCalendarDetector|true +detectorStringConcatenation=StringConcatenation|true +detectorSuperfluousInstanceOf=SuperfluousInstanceOf|true +detectorSuspiciousThreadInterrupted=SuspiciousThreadInterrupted|true +detectorSwitchFallthrough=SwitchFallthrough|true +detectorSynchronizeAndNullCheckField=SynchronizeAndNullCheckField|true +detectorSynchronizeOnClassLiteralNotGetClass=SynchronizeOnClassLiteralNotGetClass|true +detectorSynchronizingOnContentsOfFieldToProtectField=SynchronizingOnContentsOfFieldToProtectField|true +detectorURLProblems=URLProblems|true +detectorUncallableMethodOfAnonymousClass=UncallableMethodOfAnonymousClass|true +detectorUnnecessaryMath=UnnecessaryMath|true +detectorUnreadFields=UnreadFields|true +detectorUseObjectEquals=UseObjectEquals|false +detectorUselessSubclassMethod=UselessSubclassMethod|false +detectorVarArgsProblems=VarArgsProblems|true +detectorVolatileUsage=VolatileUsage|true +detectorWaitInLoop=WaitInLoop|true +detectorWrongMapIterator=WrongMapIterator|true +detectorXMLFactoryBypass=XMLFactoryBypass|true +detector_threshold=2 +effort=default +excludefilter0=findBugs/FindBugsExcludeFilter.xml +filter_settings=Medium|BAD_PRACTICE,CORRECTNESS,MT_CORRECTNESS,PERFORMANCE,STYLE|false +filter_settings_neg=MALICIOUS_CODE,NOISE,I18N,SECURITY,EXPERIMENTAL| +run_at_full_build=true diff --git a/org.eclipse.jgit/.gitignore b/org.eclipse.jgit/.gitignore new file mode 100644 index 0000000000..ba077a4031 --- /dev/null +++ b/org.eclipse.jgit/.gitignore @@ -0,0 +1 @@ +bin diff --git a/org.eclipse.jgit/.project b/org.eclipse.jgit/.project new file mode 100644 index 0000000000..19aeef1fb8 --- /dev/null +++ b/org.eclipse.jgit/.project @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>org.eclipse.jgit</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.ManifestBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.SchemaBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + <nature>org.eclipse.pde.PluginNature</nature> + </natures> +</projectDescription> diff --git a/org.eclipse.jgit/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000000..66ac15c47c --- /dev/null +++ b/org.eclipse.jgit/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +#Mon Aug 11 16:46:12 PDT 2008 +eclipse.preferences.version=1 +encoding/<project>=UTF-8 diff --git a/org.eclipse.jgit/.settings/org.eclipse.core.runtime.prefs b/org.eclipse.jgit/.settings/org.eclipse.core.runtime.prefs new file mode 100644 index 0000000000..cce05685b4 --- /dev/null +++ b/org.eclipse.jgit/.settings/org.eclipse.core.runtime.prefs @@ -0,0 +1,3 @@ +#Mon Mar 24 18:55:50 EDT 2008
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs b/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000..8e8e17240c --- /dev/null +++ b/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,321 @@ +#Sun Mar 15 01:13:43 CET 2009 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.doc.comment.support=enabled +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=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=warning +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=error +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=error +org.eclipse.jdt.core.compiler.problem.invalidJavadoc=error +org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=private +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=error +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingJavadocComments=error +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=protected +org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=return_tag +org.eclipse.jdt.core.compiler.problem.missingJavadocTags=error +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=private +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=error +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=error +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=error +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=ignore +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=warning +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=error +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=warning +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=error +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=error +org.eclipse.jdt.core.compiler.problem.unusedLabel=error +org.eclipse.jdt.core.compiler.problem.unusedLocal=error +org.eclipse.jdt.core.compiler.problem.unusedParameter=warning +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=error +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=error +org.eclipse.jdt.core.compiler.source=1.5 +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=0 +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=80 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 +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=1 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +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_type_declarations=1 +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=false +org.eclipse.jdt.core.formatter.comment.format_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_source_code=true +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true +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=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=false +org.eclipse.jdt.core.formatter.indentation.size=4 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation=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=do not 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=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=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=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=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_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.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=false +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.lineSplit=80 +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=1 +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=tab +org.eclipse.jdt.core.formatter.tabulation.size=4 +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false diff --git a/org.eclipse.jgit/.settings/org.eclipse.jdt.ui.prefs b/org.eclipse.jgit/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 0000000000..709a44074c --- /dev/null +++ b/org.eclipse.jgit/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,9 @@ +#Wed May 09 00:20:24 CEST 2007 +eclipse.preferences.version=1 +formatter_profile=_JGit +formatter_settings_version=10 +org.eclipse.jdt.ui.ignorelowercasenames=true +org.eclipse.jdt.ui.importorder=java;javax;org;com; +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"?><templates/> diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..fed005ccd5 --- /dev/null +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -0,0 +1,21 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %plugin_name +Bundle-SymbolicName: org.eclipse.jgit +Bundle-Version: 0.5.0.qualifier +Bundle-Localization: plugin +Bundle-Vendor: %provider_name +Export-Package: org.eclipse.jgit.dircache, + org.eclipse.jgit.errors;uses:="org.eclipse.jgit.lib", + org.eclipse.jgit.lib, + org.eclipse.jgit.revplot, + org.eclipse.jgit.revwalk, + org.eclipse.jgit.revwalk.filter, + org.eclipse.jgit.transport;uses:="org.eclipse.jgit.lib", + org.eclipse.jgit.treewalk, + org.eclipse.jgit.treewalk.filter, + org.eclipse.jgit.util +Bundle-ActivationPolicy: lazy +Bundle-RequiredExecutionEnvironment: J2SE-1.5 +Bundle-ClassPath: . +Require-Bundle: com.jcraft.jsch;visibility:=reexport diff --git a/org.eclipse.jgit/build.properties b/org.eclipse.jgit/build.properties new file mode 100644 index 0000000000..aa1a008269 --- /dev/null +++ b/org.eclipse.jgit/build.properties @@ -0,0 +1,5 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + plugin.properties diff --git a/org.eclipse.jgit/findBugs/FindBugsExcludeFilter.xml b/org.eclipse.jgit/findBugs/FindBugsExcludeFilter.xml new file mode 100644 index 0000000000..ba9d1d0996 --- /dev/null +++ b/org.eclipse.jgit/findBugs/FindBugsExcludeFilter.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<FindBugsFilter> + <!-- Silence PackFile.mmap calls GC, we need to force it to remove stale + memory mapped segments if the JVM heap is out of address space. + --> + <Match> + <Class name="org.eclipse.jgit.lib.PackFile" /> + <Method name="mmap" /> + <Bug pattern="DM_GC" /> + </Match> + + <!-- Silence the construction of our magic String instance. + --> + <Match> + <Class name="org.eclipse.jgit.lib.Config" /> + <Bug pattern="DM_STRING_VOID_CTOR"/> + </Match> + + <!-- Silence comparison of string by == or !=. This class is built + only to provide compare of string values, we won't make a mistake + here with == assuming .equals() style equality. + --> + <Match> + <Class name="org.eclipse.jgit.lib.util.StringUtils" /> + <Bug pattern="ES_COMPARING_PARAMETER_STRING_WITH_EQ" /> + </Match> +</FindBugsFilter> diff --git a/org.eclipse.jgit/plugin.properties b/org.eclipse.jgit/plugin.properties new file mode 100644 index 0000000000..b075cc6f2a --- /dev/null +++ b/org.eclipse.jgit/plugin.properties @@ -0,0 +1,2 @@ +plugin_name=Java Git Core +provider_name=eclipse.org diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/awtui/AWTPlotRenderer.java b/org.eclipse.jgit/src/org/eclipse/jgit/awtui/AWTPlotRenderer.java new file mode 100644 index 0000000000..647b103d28 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/awtui/AWTPlotRenderer.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.awtui; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Polygon; + +import org.eclipse.jgit.awtui.CommitGraphPane.GraphCellRender; +import org.eclipse.jgit.awtui.SwingCommitList.SwingLane; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revplot.AbstractPlotRenderer; +import org.eclipse.jgit.revplot.PlotCommit; + +final class AWTPlotRenderer extends AbstractPlotRenderer<SwingLane, Color> { + + final GraphCellRender cell; + + Graphics2D g; + + AWTPlotRenderer(final GraphCellRender c) { + cell = c; + } + + void paint(final Graphics in, final PlotCommit<SwingLane> commit) { + g = (Graphics2D) in.create(); + try { + final int h = cell.getHeight(); + g.setColor(cell.getBackground()); + g.fillRect(0, 0, cell.getWidth(), h); + if (commit != null) + paintCommit(commit, h); + } finally { + g.dispose(); + g = null; + } + } + + @Override + protected void drawLine(final Color color, int x1, int y1, int x2, + int y2, int width) { + if (y1 == y2) { + x1 -= width / 2; + x2 -= width / 2; + } else if (x1 == x2) { + y1 -= width / 2; + y2 -= width / 2; + } + + g.setColor(color); + g.setStroke(CommitGraphPane.stroke(width)); + g.drawLine(x1, y1, x2, y2); + } + + @Override + protected void drawCommitDot(final int x, final int y, final int w, + final int h) { + g.setColor(Color.blue); + g.setStroke(CommitGraphPane.strokeCache[1]); + g.fillOval(x, y, w, h); + g.setColor(Color.black); + g.drawOval(x, y, w, h); + } + + @Override + protected void drawBoundaryDot(final int x, final int y, final int w, + final int h) { + g.setColor(cell.getBackground()); + g.setStroke(CommitGraphPane.strokeCache[1]); + g.fillOval(x, y, w, h); + g.setColor(Color.black); + g.drawOval(x, y, w, h); + } + + @Override + protected void drawText(final String msg, final int x, final int y) { + final int texth = g.getFontMetrics().getHeight(); + final int y0 = y - texth/2 + (cell.getHeight() - texth)/2; + g.setColor(cell.getForeground()); + g.drawString(msg, x, y0 + texth - g.getFontMetrics().getDescent()); + } + + @Override + protected Color laneColor(final SwingLane myLane) { + return myLane != null ? myLane.color : Color.black; + } + + void paintTriangleDown(final int cx, final int y, final int h) { + final int tipX = cx; + final int tipY = y + h; + final int baseX1 = cx - 10 / 2; + final int baseX2 = tipX + 10 / 2; + final int baseY = y; + final Polygon triangle = new Polygon(); + triangle.addPoint(tipX, tipY); + triangle.addPoint(baseX1, baseY); + triangle.addPoint(baseX2, baseY); + g.fillPolygon(triangle); + g.drawPolygon(triangle); + } + + @Override + protected int drawLabel(int x, int y, Ref ref) { + String txt; + String name = ref.getOrigName(); + if (name.startsWith(Constants.R_HEADS)) { + g.setBackground(Color.GREEN); + txt = name.substring(Constants.R_HEADS.length()); + } else if (name.startsWith(Constants.R_REMOTES)){ + g.setBackground(Color.LIGHT_GRAY); + txt = name.substring(Constants.R_REMOTES.length()); + } else if (name.startsWith(Constants.R_TAGS)){ + g.setBackground(Color.YELLOW); + txt = name.substring(Constants.R_TAGS.length()); + } else { + // Whatever this would be + g.setBackground(Color.WHITE); + if (name.startsWith(Constants.R_REFS)) + txt = name.substring(Constants.R_REFS.length()); + else + txt = name; // HEAD and such + } + if (ref.getPeeledObjectId() != null) { + float[] colorComponents = g.getBackground().getRGBColorComponents(null); + colorComponents[0] *= 0.9; + colorComponents[1] *= 0.9; + colorComponents[2] *= 0.9; + g.setBackground(new Color(colorComponents[0],colorComponents[1],colorComponents[2])); + } + if (txt.length() > 12) + txt = txt.substring(0,11) + "\u2026"; // ellipsis "…" (in UTF-8) + + final int texth = g.getFontMetrics().getHeight(); + int textw = g.getFontMetrics().stringWidth(txt); + g.setColor(g.getBackground()); + int arcHeight = texth/4; + int y0 = y - texth/2 + (cell.getHeight() - texth)/2; + g.fillRoundRect(x , y0, textw + arcHeight*2, texth -1, arcHeight, arcHeight); + g.setColor(g.getColor().darker()); + g.drawRoundRect(x, y0, textw + arcHeight*2, texth -1 , arcHeight, arcHeight); + g.setColor(Color.BLACK); + g.drawString(txt, x + arcHeight, y0 + texth - g.getFontMetrics().getDescent()); + + return arcHeight * 3 + textw; + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/awtui/AwtAuthenticator.java b/org.eclipse.jgit/src/org/eclipse/jgit/awtui/AwtAuthenticator.java new file mode 100644 index 0000000000..ccd47ddda2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/awtui/AwtAuthenticator.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.awtui; + +import java.awt.Container; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.util.ArrayList; +import java.util.Collection; + +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.JTextField; + +/** Basic network prompt for username/password when using AWT. */ +public class AwtAuthenticator extends Authenticator { + private static final AwtAuthenticator me = new AwtAuthenticator(); + + /** Install this authenticator implementation into the JVM. */ + public static void install() { + setDefault(me); + } + + /** + * Add a cached authentication for future use. + * + * @param ca + * the information we should remember. + */ + public static void add(final CachedAuthentication ca) { + synchronized (me) { + me.cached.add(ca); + } + } + + private final Collection<CachedAuthentication> cached = new ArrayList<CachedAuthentication>(); + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + for (final CachedAuthentication ca : cached) { + if (ca.host.equals(getRequestingHost()) + && ca.port == getRequestingPort()) + return ca.toPasswordAuthentication(); + } + + final GridBagConstraints gbc = new GridBagConstraints(0, 0, 1, 1, 1, 1, + GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0); + final Container panel = new JPanel(); + panel.setLayout(new GridBagLayout()); + + final StringBuilder instruction = new StringBuilder(); + instruction.append("Enter username and password for "); + if (getRequestorType() == RequestorType.PROXY) { + instruction.append(getRequestorType()); + instruction.append(" "); + instruction.append(getRequestingHost()); + if (getRequestingPort() > 0) { + instruction.append(":"); + instruction.append(getRequestingPort()); + } + } else { + instruction.append(getRequestingURL()); + } + + gbc.weightx = 1.0; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.gridx = 0; + panel.add(new JLabel(instruction.toString()), gbc); + gbc.gridy++; + + gbc.gridwidth = GridBagConstraints.RELATIVE; + + // Username + // + final JTextField username; + gbc.fill = GridBagConstraints.NONE; + gbc.gridx = 0; + gbc.weightx = 1; + panel.add(new JLabel("Username:"), gbc); + + gbc.gridx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weighty = 1; + username = new JTextField(20); + panel.add(username, gbc); + gbc.gridy++; + + // Password + // + final JPasswordField password; + gbc.fill = GridBagConstraints.NONE; + gbc.gridx = 0; + gbc.weightx = 1; + panel.add(new JLabel("Password:"), gbc); + + gbc.gridx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weighty = 1; + password = new JPasswordField(20); + panel.add(password, gbc); + gbc.gridy++; + + if (JOptionPane.showConfirmDialog(null, panel, + "Authentication Required", JOptionPane.OK_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE) == JOptionPane.OK_OPTION) { + final CachedAuthentication ca = new CachedAuthentication( + getRequestingHost(), getRequestingPort(), username + .getText(), new String(password.getPassword())); + cached.add(ca); + return ca.toPasswordAuthentication(); + } + + return null; // cancel + } + + /** Authentication data to remember and reuse. */ + public static class CachedAuthentication { + final String host; + + final int port; + + final String user; + + final String pass; + + /** + * Create a new cached authentication. + * + * @param aHost + * system this is for. + * @param aPort + * port number of the service. + * @param aUser + * username at the service. + * @param aPass + * password at the service. + */ + public CachedAuthentication(final String aHost, final int aPort, + final String aUser, final String aPass) { + host = aHost; + port = aPort; + user = aUser; + pass = aPass; + } + + PasswordAuthentication toPasswordAuthentication() { + return new PasswordAuthentication(user, pass.toCharArray()); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/awtui/CommitGraphPane.java b/org.eclipse.jgit/src/org/eclipse/jgit/awtui/CommitGraphPane.java new file mode 100644 index 0000000000..8d78e47df1 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/awtui/CommitGraphPane.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.awtui; + +import java.awt.BasicStroke; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Stroke; +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.JTableHeader; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; +import javax.swing.table.TableModel; + +import org.eclipse.jgit.awtui.SwingCommitList.SwingLane; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revplot.PlotCommit; +import org.eclipse.jgit.revplot.PlotCommitList; + +/** + * Draws a commit graph in a JTable. + * <p> + * This class is currently a very primitive commit visualization tool. It shows + * a table of 3 columns: + * <ol> + * <li>Commit graph and short message</li> + * <li>Author name and email address</li> + * <li>Author date and time</li> + * </ul> + */ +public class CommitGraphPane extends JTable { + private static final long serialVersionUID = 1L; + + private final SwingCommitList allCommits; + + /** Create a new empty panel. */ + public CommitGraphPane() { + allCommits = new SwingCommitList(); + configureHeader(); + setShowHorizontalLines(false); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + configureRowHeight(); + } + + private void configureRowHeight() { + int h = 0; + for (int i = 0; i<getColumnCount(); ++i) { + TableCellRenderer renderer = getDefaultRenderer(getColumnClass(i)); + Component c = renderer.getTableCellRendererComponent(this, "Ã…Oj", false, false, 0, i); + h = Math.max(h, c.getPreferredSize().height); + } + setRowHeight(h + getRowMargin()); + } + + /** + * Get the commit list this pane renders from. + * + * @return the list the caller must populate. + */ + public PlotCommitList getCommitList() { + return allCommits; + } + + @Override + public void setModel(final TableModel dataModel) { + if (dataModel != null && !(dataModel instanceof CommitTableModel)) + throw new ClassCastException("Must be special table model."); + super.setModel(dataModel); + } + + @Override + protected TableModel createDefaultDataModel() { + return new CommitTableModel(); + } + + private void configureHeader() { + final JTableHeader th = getTableHeader(); + final TableColumnModel cols = th.getColumnModel(); + + final TableColumn graph = cols.getColumn(0); + final TableColumn author = cols.getColumn(1); + final TableColumn date = cols.getColumn(2); + + graph.setHeaderValue(""); + author.setHeaderValue("Author"); + date.setHeaderValue("Date"); + + graph.setCellRenderer(new GraphCellRender()); + author.setCellRenderer(new NameCellRender()); + date.setCellRenderer(new DateCellRender()); + } + + class CommitTableModel extends AbstractTableModel { + private static final long serialVersionUID = 1L; + + PlotCommit<SwingLane> lastCommit; + + PersonIdent lastAuthor; + + public int getColumnCount() { + return 3; + } + + public int getRowCount() { + return allCommits != null ? allCommits.size() : 0; + } + + public Object getValueAt(final int rowIndex, final int columnIndex) { + final PlotCommit<SwingLane> c = allCommits.get(rowIndex); + switch (columnIndex) { + case 0: + return c; + case 1: + return authorFor(c); + case 2: + return authorFor(c); + default: + return null; + } + } + + PersonIdent authorFor(final PlotCommit<SwingLane> c) { + if (c != lastCommit) { + lastCommit = c; + lastAuthor = c.getAuthorIdent(); + } + return lastAuthor; + } + } + + class NameCellRender extends DefaultTableCellRenderer { + private static final long serialVersionUID = 1L; + + public Component getTableCellRendererComponent(final JTable table, + final Object value, final boolean isSelected, + final boolean hasFocus, final int row, final int column) { + final PersonIdent pi = (PersonIdent) value; + + final String valueStr; + if (pi != null) + valueStr = pi.getName() + " <" + pi.getEmailAddress() + ">"; + else + valueStr = ""; + return super.getTableCellRendererComponent(table, valueStr, + isSelected, hasFocus, row, column); + } + } + + class DateCellRender extends DefaultTableCellRenderer { + private static final long serialVersionUID = 1L; + + private final DateFormat fmt = new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss"); + + public Component getTableCellRendererComponent(final JTable table, + final Object value, final boolean isSelected, + final boolean hasFocus, final int row, final int column) { + final PersonIdent pi = (PersonIdent) value; + + final String valueStr; + if (pi != null) + valueStr = fmt.format(pi.getWhen()); + else + valueStr = ""; + return super.getTableCellRendererComponent(table, valueStr, + isSelected, hasFocus, row, column); + } + } + + class GraphCellRender extends DefaultTableCellRenderer { + private static final long serialVersionUID = 1L; + + private final AWTPlotRenderer renderer = new AWTPlotRenderer(this); + + PlotCommit<SwingLane> commit; + + public Component getTableCellRendererComponent(final JTable table, + final Object value, final boolean isSelected, + final boolean hasFocus, final int row, final int column) { + super.getTableCellRendererComponent(table, value, isSelected, + hasFocus, row, column); + commit = (PlotCommit<SwingLane>) value; + return this; + } + + @Override + protected void paintComponent(final Graphics inputGraphics) { + if (inputGraphics == null) + return; + renderer.paint(inputGraphics, commit); + } + } + + static final Stroke[] strokeCache; + + static { + strokeCache = new Stroke[4]; + for (int i = 1; i < strokeCache.length; i++) + strokeCache[i] = new BasicStroke(i); + } + + static Stroke stroke(final int width) { + if (width < strokeCache.length) + return strokeCache[width]; + return new BasicStroke(width); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/awtui/SwingCommitList.java b/org.eclipse.jgit/src/org/eclipse/jgit/awtui/SwingCommitList.java new file mode 100644 index 0000000000..4a11964473 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/awtui/SwingCommitList.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.awtui; + +import java.awt.Color; +import java.util.LinkedList; + +import org.eclipse.jgit.revplot.PlotCommitList; +import org.eclipse.jgit.revplot.PlotLane; + +class SwingCommitList extends PlotCommitList<SwingCommitList.SwingLane> { + final LinkedList<Color> colors; + + SwingCommitList() { + colors = new LinkedList<Color>(); + repackColors(); + } + + private void repackColors() { + colors.add(Color.green); + colors.add(Color.blue); + colors.add(Color.red); + colors.add(Color.magenta); + colors.add(Color.darkGray); + colors.add(Color.yellow.darker()); + colors.add(Color.orange); + } + + @Override + protected SwingLane createLane() { + final SwingLane lane = new SwingLane(); + if (colors.isEmpty()) + repackColors(); + lane.color = colors.removeFirst(); + return lane; + } + + @Override + protected void recycleLane(final SwingLane lane) { + colors.add(lane.color); + } + + static class SwingLane extends PlotLane { + Color color; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java new file mode 100644 index 0000000000..639ed77ee8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008-2009, Johannes E. Schindelin <johannes.schindelin@gmx.de> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.diff; + +import static org.eclipse.jgit.lib.Constants.encodeASCII; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +import org.eclipse.jgit.patch.FileHeader; + +/** + * Format an {@link EditList} as a Git style unified patch script. + */ +public class DiffFormatter { + private static final byte[] noNewLine = encodeASCII("\\ No newline at end of file\n"); + + private int context; + + /** Create a new formatter with a default level of context. */ + public DiffFormatter() { + setContext(3); + } + + /** + * Change the number of lines of context to display. + * + * @param lineCount + * number of lines of context to see before the first + * modification and after the last modification within a hunk of + * the modified file. + */ + public void setContext(final int lineCount) { + if (lineCount < 0) + throw new IllegalArgumentException("context must be >= 0"); + context = lineCount; + } + + /** + * Format a patch script, reusing a previously parsed FileHeader. + * <p> + * This formatter is primarily useful for editing an existing patch script + * to increase or reduce the number of lines of context within the script. + * All header lines are reused as-is from the supplied FileHeader. + * + * @param out + * stream to write the patch script out to. + * @param head + * existing file header containing the header lines to copy. + * @param a + * text source for the pre-image version of the content. This + * must match the content of {@link FileHeader#getOldId()}. + * @param b + * text source for the post-image version of the content. This + * must match the content of {@link FileHeader#getNewId()}. + * @throws IOException + * writing to the supplied stream failed. + */ + public void format(final OutputStream out, final FileHeader head, + final RawText a, final RawText b) throws IOException { + // Reuse the existing FileHeader as-is by blindly copying its + // header lines, but avoiding its hunks. Instead we recreate + // the hunks from the text instances we have been supplied. + // + final int start = head.getStartOffset(); + int end = head.getEndOffset(); + if (!head.getHunks().isEmpty()) + end = head.getHunks().get(0).getStartOffset(); + out.write(head.getBuffer(), start, end - start); + + formatEdits(out, a, b, head.toEditList()); + } + + private void formatEdits(final OutputStream out, final RawText a, + final RawText b, final EditList edits) throws IOException { + for (int curIdx = 0; curIdx < edits.size();) { + Edit curEdit = edits.get(curIdx); + final int endIdx = findCombinedEnd(edits, curIdx); + final Edit endEdit = edits.get(endIdx); + + int aCur = Math.max(0, curEdit.getBeginA() - context); + int bCur = Math.max(0, curEdit.getBeginB() - context); + final int aEnd = Math.min(a.size(), endEdit.getEndA() + context); + final int bEnd = Math.min(b.size(), endEdit.getEndB() + context); + + writeHunkHeader(out, aCur, aEnd, bCur, bEnd); + + while (aCur < aEnd || bCur < bEnd) { + if (aCur < curEdit.getBeginA() || endIdx + 1 < curIdx) { + writeLine(out, ' ', a, aCur); + aCur++; + bCur++; + + } else if (aCur < curEdit.getEndA()) { + writeLine(out, '-', a, aCur++); + + } else if (bCur < curEdit.getEndB()) { + writeLine(out, '+', b, bCur++); + } + + if (end(curEdit, aCur, bCur) && ++curIdx < edits.size()) + curEdit = edits.get(curIdx); + } + } + } + + private void writeHunkHeader(final OutputStream out, int aCur, int aEnd, + int bCur, int bEnd) throws IOException { + out.write('@'); + out.write('@'); + writeRange(out, '-', aCur + 1, aEnd - aCur); + writeRange(out, '+', bCur + 1, bEnd - bCur); + out.write(' '); + out.write('@'); + out.write('@'); + out.write('\n'); + } + + private static void writeRange(final OutputStream out, final char prefix, + final int begin, final int cnt) throws IOException { + out.write(' '); + out.write(prefix); + switch (cnt) { + case 0: + // If the range is empty, its beginning number must be the + // line just before the range, or 0 if the range is at the + // start of the file stream. Here, begin is always 1 based, + // so an empty file would produce "0,0". + // + out.write(encodeASCII(begin - 1)); + out.write(','); + out.write('0'); + break; + + case 1: + // If the range is exactly one line, produce only the number. + // + out.write(encodeASCII(begin)); + break; + + default: + out.write(encodeASCII(begin)); + out.write(','); + out.write(encodeASCII(cnt)); + break; + } + } + + private static void writeLine(final OutputStream out, final char prefix, + final RawText text, final int cur) throws IOException { + out.write(prefix); + text.writeLine(out, cur); + out.write('\n'); + if (cur + 1 == text.size() && text.isMissingNewlineAtEnd()) + out.write(noNewLine); + } + + private int findCombinedEnd(final List<Edit> edits, final int i) { + int end = i + 1; + while (end < edits.size() + && (combineA(edits, end) || combineB(edits, end))) + end++; + return end - 1; + } + + private boolean combineA(final List<Edit> e, final int i) { + return e.get(i).getBeginA() - e.get(i - 1).getEndA() <= 2 * context; + } + + private boolean combineB(final List<Edit> e, final int i) { + return e.get(i).getBeginB() - e.get(i - 1).getEndB() <= 2 * context; + } + + private static boolean end(final Edit edit, final int a, final int b) { + return edit.getEndA() <= a && edit.getEndB() <= b; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/Edit.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/Edit.java new file mode 100644 index 0000000000..109c049ccd --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/Edit.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2008-2009, Johannes E. Schindelin <johannes.schindelin@gmx.de> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.diff; + +/** + * A modified region detected between two versions of roughly the same content. + * <p> + * An edit covers the modified region only. It does not cover a common region. + * <p> + * Regions should be specified using 0 based notation, so add 1 to the start and + * end marks for line numbers in a file. + * <p> + * An edit where <code>beginA == endA && beginB < endB</code> is an insert edit, + * that is sequence B inserted the elements in region + * <code>[beginB, endB)</code> at <code>beginA</code>. + * <p> + * An edit where <code>beginA < endA && beginB == endB</code> is a delete edit, + * that is sequence B has removed the elements between + * <code>[beginA, endA)</code>. + * <p> + * An edit where <code>beginA < endA && beginB < endB</code> is a replace edit, + * that is sequence B has replaced the range of elements between + * <code>[beginA, endA)</code> with those found in <code>[beginB, endB)</code>. + */ +public class Edit { + /** Type of edit */ + public static enum Type { + /** Sequence B has inserted the region. */ + INSERT, + + /** Sequence B has removed the region. */ + DELETE, + + /** Sequence B has replaced the region with different content. */ + REPLACE, + + /** Sequence A and B have zero length, describing nothing. */ + EMPTY; + } + + int beginA; + + int endA; + + int beginB; + + int endB; + + /** + * Create a new empty edit. + * + * @param as + * beginA: start and end of region in sequence A; 0 based. + * @param bs + * beginB: start and end of region in sequence B; 0 based. + */ + public Edit(final int as, final int bs) { + this(as, as, bs, bs); + } + + /** + * Create a new edit. + * + * @param as + * beginA: start of region in sequence A; 0 based. + * @param ae + * endA: end of region in sequence A; must be >= as. + * @param bs + * beginB: start of region in sequence B; 0 based. + * @param be + * endB: end of region in sequence B; must be >= bs. + */ + public Edit(final int as, final int ae, final int bs, final int be) { + beginA = as; + endA = ae; + + beginB = bs; + endB = be; + } + + /** @return the type of this region */ + public final Type getType() { + if (beginA == endA && beginB < endB) + return Type.INSERT; + if (beginA < endA && beginB == endB) + return Type.DELETE; + if (beginA == endA && beginB == endB) + return Type.EMPTY; + return Type.REPLACE; + } + + /** @return start point in sequence A. */ + public final int getBeginA() { + return beginA; + } + + /** @return end point in sequence A. */ + public final int getEndA() { + return endA; + } + + /** @return start point in sequence B. */ + public final int getBeginB() { + return beginB; + } + + /** @return end point in sequence B. */ + public final int getEndB() { + return endB; + } + + /** Increase {@link #getEndA()} by 1. */ + public void extendA() { + endA++; + } + + /** Increase {@link #getEndB()} by 1. */ + public void extendB() { + endB++; + } + + /** Swap A and B, so the edit goes the other direction. */ + public void swap() { + final int sBegin = beginA; + final int sEnd = endA; + + beginA = beginB; + endA = endB; + + beginB = sBegin; + endB = sEnd; + } + + @Override + public int hashCode() { + return beginA ^ endA; + } + + @Override + public boolean equals(final Object o) { + if (o instanceof Edit) { + final Edit e = (Edit) o; + return this.beginA == e.beginA && this.endA == e.endA + && this.beginB == e.beginB && this.endB == e.endB; + } + return false; + } + + @Override + public String toString() { + final Type t = getType(); + return t + "(" + beginA + "-" + endA + "," + beginB + "-" + endB + ")"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/EditList.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/EditList.java new file mode 100644 index 0000000000..85a5396440 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/EditList.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.diff; + +import java.util.AbstractList; +import java.util.ArrayList; + +/** Specialized list of {@link Edit}s in a document. */ +public class EditList extends AbstractList<Edit> { + private final ArrayList<Edit> container; + + /** Create a new, empty edit list. */ + public EditList() { + container = new ArrayList<Edit>(); + } + + @Override + public int size() { + return container.size(); + } + + @Override + public Edit get(final int index) { + return container.get(index); + } + + @Override + public Edit set(final int index, final Edit element) { + return container.set(index, element); + } + + @Override + public void add(final int index, final Edit element) { + container.add(index, element); + } + + @Override + public Edit remove(final int index) { + return container.remove(index); + } + + @Override + public int hashCode() { + return container.hashCode(); + } + + @Override + public boolean equals(final Object o) { + if (o instanceof EditList) + return container.equals(((EditList) o).container); + return false; + } + + @Override + public String toString() { + return "EditList" + container.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java new file mode 100644 index 0000000000..61a3ef41cd --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008-2009, Johannes E. Schindelin <johannes.schindelin@gmx.de> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.diff; + +import java.io.IOException; +import java.io.OutputStream; + +import org.eclipse.jgit.util.IntList; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * A Sequence supporting UNIX formatted text in byte[] format. + * <p> + * Elements of the sequence are the lines of the file, as delimited by the UNIX + * newline character ('\n'). The file content is treated as 8 bit binary text, + * with no assumptions or requirements on character encoding. + * <p> + * Note that the first line of the file is element 0, as defined by the Sequence + * interface API. Traditionally in a text editor a patch file the first line is + * line number 1. Callers may need to subtract 1 prior to invoking methods if + * they are converting from "line number" to "element index". + */ +public class RawText implements Sequence { + /** The file content for this sequence. */ + protected final byte[] content; + + /** Map of line number to starting position within {@link #content}. */ + protected final IntList lines; + + /** Hash code for each line, for fast equality elimination. */ + protected final IntList hashes; + + /** + * Create a new sequence from an existing content byte array. + * <p> + * The entire array (indexes 0 through length-1) is used as the content. + * + * @param input + * the content array. The array is never modified, so passing + * through cached arrays is safe. + */ + public RawText(final byte[] input) { + content = input; + lines = RawParseUtils.lineMap(content, 0, content.length); + hashes = computeHashes(); + } + + public int size() { + // The line map is always 2 entries larger than the number of lines in + // the file. Index 0 is padded out/unused. The last index is the total + // length of the buffer, and acts as a sentinel. + // + return lines.size() - 2; + } + + public boolean equals(final int i, final Sequence other, final int j) { + return equals(this, i + 1, (RawText) other, j + 1); + } + + private static boolean equals(final RawText a, final int ai, + final RawText b, final int bi) { + if (a.hashes.get(ai) != b.hashes.get(bi)) + return false; + + int as = a.lines.get(ai); + int bs = b.lines.get(bi); + final int ae = a.lines.get(ai + 1); + final int be = b.lines.get(bi + 1); + + if (ae - as != be - bs) + return false; + + while (as < ae) { + if (a.content[as++] != b.content[bs++]) + return false; + } + return true; + } + + /** + * Write a specific line to the output stream, without its trailing LF. + * <p> + * The specified line is copied as-is, with no character encoding + * translation performed. + * <p> + * If the specified line ends with an LF ('\n'), the LF is <b>not</b> + * copied. It is up to the caller to write the LF, if desired, between + * output lines. + * + * @param out + * stream to copy the line data onto. + * @param i + * index of the line to extract. Note this is 0-based, so line + * number 1 is actually index 0. + * @throws IOException + * the stream write operation failed. + */ + public void writeLine(final OutputStream out, final int i) + throws IOException { + final int start = lines.get(i + 1); + int end = lines.get(i + 2); + if (content[end - 1] == '\n') + end--; + out.write(content, start, end - start); + } + + /** + * Determine if the file ends with a LF ('\n'). + * + * @return true if the last line has an LF; false otherwise. + */ + public boolean isMissingNewlineAtEnd() { + final int end = lines.get(lines.size() - 1); + if (end == 0) + return true; + return content[end - 1] != '\n'; + } + + private IntList computeHashes() { + final IntList r = new IntList(lines.size()); + r.add(0); + for (int lno = 1; lno < lines.size() - 1; lno++) { + final int ptr = lines.get(lno); + final int end = lines.get(lno + 1); + r.add(hashLine(content, ptr, end)); + } + r.add(0); + return r; + } + + /** + * Compute a hash code for a single line. + * + * @param raw + * the raw file content. + * @param ptr + * first byte of the content line to hash. + * @param end + * 1 past the last byte of the content line. + * @return hash code for the region <code>[ptr, end)</code> of raw. + */ + protected int hashLine(final byte[] raw, int ptr, final int end) { + int hash = 5381; + for (; ptr < end; ptr++) + hash = (hash << 5) ^ (raw[ptr] & 0xff); + return hash; + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/Sequence.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/Sequence.java new file mode 100644 index 0000000000..3a33564113 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/Sequence.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2008-2009, Johannes E. Schindelin <johannes.schindelin@gmx.de> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.diff; + +/** + * Arbitrary sequence of elements with fast comparison support. + * <p> + * A sequence of elements is defined to contain elements in the index range + * <code>[0, {@link #size()})</code>, like a standard Java List implementation. + * Unlike a List, the members of the sequence are not directly obtainable, but + * element equality can be tested if two Sequences are the same implementation. + * <p> + * An implementation may chose to implement the equals semantic as necessary, + * including fuzzy matching rules such as ignoring insignificant sub-elements, + * e.g. ignoring whitespace differences in text. + * <p> + * Implementations of Sequence are primarily intended for use in content + * difference detection algorithms, to produce an {@link EditList} of + * {@link Edit} instances describing how two Sequence instances differ. + */ +public interface Sequence { + /** @return total number of items in the sequence. */ + public int size(); + + /** + * Determine if the i-th member is equal to the j-th member. + * <p> + * Implementations must ensure <code>equals(thisIdx,other,otherIdx)</code> + * returns the same as <code>other.equals(otherIdx,this,thisIdx)</code>. + * + * @param thisIdx + * index within <code>this</code> sequence; must be in the range + * <code>[ 0, this.size() )</code>. + * @param other + * another sequence; must be the same implementation class, that + * is <code>this.getClass() == other.getClass()</code>. + * @param otherIdx + * index within <code>other</code> sequence; must be in the range + * <code>[ 0, other.size() )</code>. + * @return true if the elements are equal; false if they are not equal. + */ + public boolean equals(int thisIdx, Sequence other, int otherIdx); +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java new file mode 100644 index 0000000000..70f80aeb7a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.dircache; + +import java.io.IOException; + +/** + * Generic update/editing support for {@link DirCache}. + * <p> + * The different update strategies extend this class to provide their own unique + * services to applications. + */ +abstract class BaseDirCacheEditor { + /** The cache instance this editor updates during {@link #finish()}. */ + protected DirCache cache; + + /** + * Entry table this builder will eventually replace into {@link #cache}. + * <p> + * Use {@link #fastAdd(DirCacheEntry)} or {@link #fastKeep(int, int)} to + * make additions to this table. The table is automatically expanded if it + * is too small for a new addition. + * <p> + * Typically the entries in here are sorted by their path names, just like + * they are in the DirCache instance. + */ + protected DirCacheEntry[] entries; + + /** Total number of valid entries in {@link #entries}. */ + protected int entryCnt; + + /** + * Construct a new editor. + * + * @param dc + * the cache this editor will eventually update. + * @param ecnt + * estimated number of entries the editor will have upon + * completion. This sizes the initial entry table. + */ + protected BaseDirCacheEditor(final DirCache dc, final int ecnt) { + cache = dc; + entries = new DirCacheEntry[ecnt]; + } + + /** + * @return the cache we will update on {@link #finish()}. + */ + public DirCache getDirCache() { + return cache; + } + + /** + * Append one entry into the resulting entry list. + * <p> + * The entry is placed at the end of the entry list. The caller is + * responsible for making sure the final table is correctly sorted. + * <p> + * The {@link #entries} table is automatically expanded if there is + * insufficient space for the new addition. + * + * @param newEntry + * the new entry to add. + */ + protected void fastAdd(final DirCacheEntry newEntry) { + if (entries.length == entryCnt) { + final DirCacheEntry[] n = new DirCacheEntry[(entryCnt + 16) * 3 / 2]; + System.arraycopy(entries, 0, n, 0, entryCnt); + entries = n; + } + entries[entryCnt++] = newEntry; + } + + /** + * Add a range of existing entries from the destination cache. + * <p> + * The entries are placed at the end of the entry list, preserving their + * current order. The caller is responsible for making sure the final table + * is correctly sorted. + * <p> + * This method copies from the destination cache, which has not yet been + * updated with this editor's new table. So all offsets into the destination + * cache are not affected by any updates that may be currently taking place + * in this editor. + * <p> + * The {@link #entries} table is automatically expanded if there is + * insufficient space for the new additions. + * + * @param pos + * first entry to copy from the destination cache. + * @param cnt + * number of entries to copy. + */ + protected void fastKeep(final int pos, int cnt) { + if (entryCnt + cnt > entries.length) { + final int m1 = (entryCnt + 16) * 3 / 2; + final int m2 = entryCnt + cnt; + final DirCacheEntry[] n = new DirCacheEntry[Math.max(m1, m2)]; + System.arraycopy(entries, 0, n, 0, entryCnt); + entries = n; + } + + cache.toArray(pos, entries, entryCnt, cnt); + entryCnt += cnt; + } + + /** + * Finish this builder and update the destination {@link DirCache}. + * <p> + * When this method completes this builder instance is no longer usable by + * the calling application. A new builder must be created to make additional + * changes to the index entries. + * <p> + * After completion the DirCache returned by {@link #getDirCache()} will + * contain all modifications. + * <p> + * <i>Note to implementors:</i> Make sure {@link #entries} is fully sorted + * then invoke {@link #replace()} to update the DirCache with the new table. + */ + public abstract void finish(); + + /** + * Update the DirCache with the contents of {@link #entries}. + * <p> + * This method should be invoked only during an implementation of + * {@link #finish()}, and only after {@link #entries} is sorted. + */ + protected void replace() { + if (entryCnt < entries.length / 2) { + final DirCacheEntry[] n = new DirCacheEntry[entryCnt]; + System.arraycopy(entries, 0, n, 0, entryCnt); + entries = n; + } + cache.replace(entries, entryCnt); + } + + /** + * Finish, write, commit this change, and release the index lock. + * <p> + * If this method fails (returns false) the lock is still released. + * <p> + * This is a utility method for applications as the finish-write-commit + * pattern is very common after using a builder to update entries. + * + * @return true if the commit was successful and the file contains the new + * data; false if the commit failed and the file remains with the + * old data. + * @throws IllegalStateException + * the lock is not held. + * @throws IOException + * the output file could not be created. The caller no longer + * holds the lock. + */ + public boolean commit() throws IOException { + finish(); + cache.write(); + return cache.commit(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java new file mode 100644 index 0000000000..d6676e41fd --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java @@ -0,0 +1,756 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.dircache; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Comparator; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.UnmergedPathException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.LockFile; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectWriter; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.MutableInteger; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.TemporaryBuffer; + +/** + * Support for the Git dircache (aka index file). + * <p> + * The index file keeps track of which objects are currently checked out in the + * working directory, and the last modified time of those working files. Changes + * in the working directory can be detected by comparing the modification times + * to the cached modification time within the index file. + * <p> + * Index files are also used during merges, where the merge happens within the + * index file first, and the working directory is updated as a post-merge step. + * Conflicts are stored in the index file to allow tool (and human) based + * resolutions to be easily performed. + */ +public class DirCache { + private static final byte[] SIG_DIRC = { 'D', 'I', 'R', 'C' }; + + private static final int EXT_TREE = 0x54524545 /* 'TREE' */; + + private static final int INFO_LEN = DirCacheEntry.INFO_LEN; + + private static final DirCacheEntry[] NO_ENTRIES = {}; + + static final Comparator<DirCacheEntry> ENT_CMP = new Comparator<DirCacheEntry>() { + public int compare(final DirCacheEntry o1, final DirCacheEntry o2) { + final int cr = cmp(o1, o2); + if (cr != 0) + return cr; + return o1.getStage() - o2.getStage(); + } + }; + + static int cmp(final DirCacheEntry a, final DirCacheEntry b) { + return cmp(a.path, a.path.length, b); + } + + static int cmp(final byte[] aPath, final int aLen, final DirCacheEntry b) { + return cmp(aPath, aLen, b.path, b.path.length); + } + + static int cmp(final byte[] aPath, final int aLen, final byte[] bPath, + final int bLen) { + for (int cPos = 0; cPos < aLen && cPos < bLen; cPos++) { + final int cmp = (aPath[cPos] & 0xff) - (bPath[cPos] & 0xff); + if (cmp != 0) + return cmp; + } + return aLen - bLen; + } + + /** + * Create a new empty index which is never stored on disk. + * + * @return an empty cache which has no backing store file. The cache may not + * be read or written, but it may be queried and updated (in + * memory). + */ + public static DirCache newInCore() { + return new DirCache(null); + } + + /** + * Create a new in-core index representation and read an index from disk. + * <p> + * The new index will be read before it is returned to the caller. Read + * failures are reported as exceptions and therefore prevent the method from + * returning a partially populated index. + * + * @param indexLocation + * location of the index file on disk. + * @return a cache representing the contents of the specified index file (if + * it exists) or an empty cache if the file does not exist. + * @throws IOException + * the index file is present but could not be read. + * @throws CorruptObjectException + * the index file is using a format or extension that this + * library does not support. + */ + public static DirCache read(final File indexLocation) + throws CorruptObjectException, IOException { + final DirCache c = new DirCache(indexLocation); + c.read(); + return c; + } + + /** + * Create a new in-core index representation and read an index from disk. + * <p> + * The new index will be read before it is returned to the caller. Read + * failures are reported as exceptions and therefore prevent the method from + * returning a partially populated index. + * + * @param db + * repository the caller wants to read the default index of. + * @return a cache representing the contents of the specified index file (if + * it exists) or an empty cache if the file does not exist. + * @throws IOException + * the index file is present but could not be read. + * @throws CorruptObjectException + * the index file is using a format or extension that this + * library does not support. + */ + public static DirCache read(final Repository db) + throws CorruptObjectException, IOException { + return read(new File(db.getDirectory(), "index")); + } + + /** + * Create a new in-core index representation, lock it, and read from disk. + * <p> + * The new index will be locked and then read before it is returned to the + * caller. Read failures are reported as exceptions and therefore prevent + * the method from returning a partially populated index. On read failure, + * the lock is released. + * + * @param indexLocation + * location of the index file on disk. + * @return a cache representing the contents of the specified index file (if + * it exists) or an empty cache if the file does not exist. + * @throws IOException + * the index file is present but could not be read, or the lock + * could not be obtained. + * @throws CorruptObjectException + * the index file is using a format or extension that this + * library does not support. + */ + public static DirCache lock(final File indexLocation) + throws CorruptObjectException, IOException { + final DirCache c = new DirCache(indexLocation); + if (!c.lock()) + throw new IOException("Cannot lock " + indexLocation); + + try { + c.read(); + } catch (IOException e) { + c.unlock(); + throw e; + } catch (RuntimeException e) { + c.unlock(); + throw e; + } catch (Error e) { + c.unlock(); + throw e; + } + + return c; + } + + /** + * Create a new in-core index representation, lock it, and read from disk. + * <p> + * The new index will be locked and then read before it is returned to the + * caller. Read failures are reported as exceptions and therefore prevent + * the method from returning a partially populated index. + * + * @param db + * repository the caller wants to read the default index of. + * @return a cache representing the contents of the specified index file (if + * it exists) or an empty cache if the file does not exist. + * @throws IOException + * the index file is present but could not be read, or the lock + * could not be obtained. + * @throws CorruptObjectException + * the index file is using a format or extension that this + * library does not support. + */ + public static DirCache lock(final Repository db) + throws CorruptObjectException, IOException { + return lock(new File(db.getDirectory(), "index")); + } + + /** Location of the current version of the index file. */ + private final File liveFile; + + /** Modification time of the file at the last read/write we did. */ + private long lastModified; + + /** Individual file index entries, sorted by path name. */ + private DirCacheEntry[] sortedEntries; + + /** Number of positions within {@link #sortedEntries} that are valid. */ + private int entryCnt; + + /** Cache tree for this index; null if the cache tree is not available. */ + private DirCacheTree tree; + + /** Our active lock (if we hold it); null if we don't have it locked. */ + private LockFile myLock; + + /** + * Create a new in-core index representation. + * <p> + * The new index will be empty. Callers may wish to read from the on disk + * file first with {@link #read()}. + * + * @param indexLocation + * location of the index file on disk. + */ + public DirCache(final File indexLocation) { + liveFile = indexLocation; + clear(); + } + + /** + * Create a new builder to update this cache. + * <p> + * Callers should add all entries to the builder, then use + * {@link DirCacheBuilder#finish()} to update this instance. + * + * @return a new builder instance for this cache. + */ + public DirCacheBuilder builder() { + return new DirCacheBuilder(this, entryCnt + 16); + } + + /** + * Create a new editor to recreate this cache. + * <p> + * Callers should add commands to the editor, then use + * {@link DirCacheEditor#finish()} to update this instance. + * + * @return a new builder instance for this cache. + */ + public DirCacheEditor editor() { + return new DirCacheEditor(this, entryCnt + 16); + } + + void replace(final DirCacheEntry[] e, final int cnt) { + sortedEntries = e; + entryCnt = cnt; + tree = null; + } + + /** + * Read the index from disk, if it has changed on disk. + * <p> + * This method tries to avoid loading the index if it has not changed since + * the last time we consulted it. A missing index file will be treated as + * though it were present but had no file entries in it. + * + * @throws IOException + * the index file is present but could not be read. This + * DirCache instance may not be populated correctly. + * @throws CorruptObjectException + * the index file is using a format or extension that this + * library does not support. + */ + public void read() throws IOException, CorruptObjectException { + if (liveFile == null) + throw new IOException("DirCache does not have a backing file"); + if (!liveFile.exists()) + clear(); + else if (liveFile.lastModified() != lastModified) { + try { + final FileInputStream inStream = new FileInputStream(liveFile); + try { + clear(); + readFrom(inStream); + } finally { + try { + inStream.close(); + } catch (IOException err2) { + // Ignore any close failures. + } + } + } catch (FileNotFoundException fnfe) { + // Someone must have deleted it between our exists test + // and actually opening the path. That's fine, its empty. + // + clear(); + } + } + } + + /** Empty this index, removing all entries. */ + public void clear() { + lastModified = 0; + sortedEntries = NO_ENTRIES; + entryCnt = 0; + tree = null; + } + + private void readFrom(final FileInputStream inStream) throws IOException, + CorruptObjectException { + final BufferedInputStream in = new BufferedInputStream(inStream); + final MessageDigest md = Constants.newMessageDigest(); + + // Read the index header and verify we understand it. + // + final byte[] hdr = new byte[20]; + NB.readFully(in, hdr, 0, 12); + md.update(hdr, 0, 12); + if (!is_DIRC(hdr)) + throw new CorruptObjectException("Not a DIRC file."); + final int ver = NB.decodeInt32(hdr, 4); + if (ver != 2) + throw new CorruptObjectException("Unknown DIRC version " + ver); + entryCnt = NB.decodeInt32(hdr, 8); + if (entryCnt < 0) + throw new CorruptObjectException("DIRC has too many entries."); + + // Load the individual file entries. + // + final byte[] infos = new byte[INFO_LEN * entryCnt]; + sortedEntries = new DirCacheEntry[entryCnt]; + for (int i = 0; i < entryCnt; i++) + sortedEntries[i] = new DirCacheEntry(infos, i * INFO_LEN, in, md); + lastModified = liveFile.lastModified(); + + // After the file entries are index extensions, and then a footer. + // + for (;;) { + in.mark(21); + NB.readFully(in, hdr, 0, 20); + if (in.read() < 0) { + // No extensions present; the file ended where we expected. + // + break; + } + in.reset(); + + switch (NB.decodeInt32(hdr, 0)) { + case EXT_TREE: { + final byte[] raw = new byte[NB.decodeInt32(hdr, 4)]; + md.update(hdr, 0, 8); + NB.skipFully(in, 8); + NB.readFully(in, raw, 0, raw.length); + md.update(raw, 0, raw.length); + tree = new DirCacheTree(raw, new MutableInteger(), null); + break; + } + default: + if (hdr[0] >= 'A' && hdr[0] <= 'Z') { + // The extension is optional and is here only as + // a performance optimization. Since we do not + // understand it, we can safely skip past it. + // + NB.skipFully(in, NB.decodeUInt32(hdr, 4)); + } else { + // The extension is not an optimization and is + // _required_ to understand this index format. + // Since we did not trap it above we must abort. + // + throw new CorruptObjectException("DIRC extension '" + + Constants.CHARSET.decode( + ByteBuffer.wrap(hdr, 0, 4)).toString() + + "' not supported by this version."); + } + } + } + + final byte[] exp = md.digest(); + if (!Arrays.equals(exp, hdr)) { + throw new CorruptObjectException("DIRC checksum mismatch"); + } + } + + private static boolean is_DIRC(final byte[] hdr) { + if (hdr.length < SIG_DIRC.length) + return false; + for (int i = 0; i < SIG_DIRC.length; i++) + if (hdr[i] != SIG_DIRC[i]) + return false; + return true; + } + + /** + * Try to establish an update lock on the cache file. + * + * @return true if the lock is now held by the caller; false if it is held + * by someone else. + * @throws IOException + * the output file could not be created. The caller does not + * hold the lock. + */ + public boolean lock() throws IOException { + if (liveFile == null) + throw new IOException("DirCache does not have a backing file"); + final LockFile tmp = new LockFile(liveFile); + if (tmp.lock()) { + tmp.setNeedStatInformation(true); + myLock = tmp; + return true; + } + return false; + } + + /** + * Write the entry records from memory to disk. + * <p> + * The cache must be locked first by calling {@link #lock()} and receiving + * true as the return value. Applications are encouraged to lock the index, + * then invoke {@link #read()} to ensure the in-memory data is current, + * prior to updating the in-memory entries. + * <p> + * Once written the lock is closed and must be either committed with + * {@link #commit()} or rolled back with {@link #unlock()}. + * + * @throws IOException + * the output file could not be created. The caller no longer + * holds the lock. + */ + public void write() throws IOException { + final LockFile tmp = myLock; + requireLocked(tmp); + try { + writeTo(new BufferedOutputStream(tmp.getOutputStream())); + } catch (IOException err) { + tmp.unlock(); + throw err; + } catch (RuntimeException err) { + tmp.unlock(); + throw err; + } catch (Error err) { + tmp.unlock(); + throw err; + } + } + + private void writeTo(final OutputStream os) throws IOException { + final MessageDigest foot = Constants.newMessageDigest(); + final DigestOutputStream dos = new DigestOutputStream(os, foot); + + // Write the header. + // + final byte[] tmp = new byte[128]; + System.arraycopy(SIG_DIRC, 0, tmp, 0, SIG_DIRC.length); + NB.encodeInt32(tmp, 4, /* version */2); + NB.encodeInt32(tmp, 8, entryCnt); + dos.write(tmp, 0, 12); + + // Write the individual file entries. + // + if (lastModified <= 0) { + // Write a new index, as no entries require smudging. + // + for (int i = 0; i < entryCnt; i++) + sortedEntries[i].write(dos); + } else { + final int smudge_s = (int) (lastModified / 1000); + final int smudge_ns = ((int) (lastModified % 1000)) * 1000000; + for (int i = 0; i < entryCnt; i++) { + final DirCacheEntry e = sortedEntries[i]; + if (e.mightBeRacilyClean(smudge_s, smudge_ns)) + e.smudgeRacilyClean(); + e.write(dos); + } + } + + if (tree != null) { + final TemporaryBuffer bb = new TemporaryBuffer(); + tree.write(tmp, bb); + bb.close(); + + NB.encodeInt32(tmp, 0, EXT_TREE); + NB.encodeInt32(tmp, 4, (int) bb.length()); + dos.write(tmp, 0, 8); + bb.writeTo(dos, null); + } + + os.write(foot.digest()); + os.close(); + } + + /** + * Commit this change and release the lock. + * <p> + * If this method fails (returns false) the lock is still released. + * + * @return true if the commit was successful and the file contains the new + * data; false if the commit failed and the file remains with the + * old data. + * @throws IllegalStateException + * the lock is not held. + */ + public boolean commit() { + final LockFile tmp = myLock; + requireLocked(tmp); + myLock = null; + if (!tmp.commit()) + return false; + lastModified = tmp.getCommitLastModified(); + return true; + } + + private void requireLocked(final LockFile tmp) { + if (liveFile == null) + throw new IllegalStateException("DirCache is not locked"); + if (tmp == null) + throw new IllegalStateException("DirCache " + + liveFile.getAbsolutePath() + " not locked."); + } + + /** + * Unlock this file and abort this change. + * <p> + * The temporary file (if created) is deleted before returning. + */ + public void unlock() { + final LockFile tmp = myLock; + if (tmp != null) { + myLock = null; + tmp.unlock(); + } + } + + /** + * Locate the position a path's entry is at in the index. + * <p> + * If there is at least one entry in the index for this path the position of + * the lowest stage is returned. Subsequent stages can be identified by + * testing consecutive entries until the path differs. + * <p> + * If no path matches the entry -(position+1) is returned, where position is + * the location it would have gone within the index. + * + * @param path + * the path to search for. + * @return if >= 0 then the return value is the position of the entry in the + * index; pass to {@link #getEntry(int)} to obtain the entry + * information. If < 0 the entry does not exist in the index. + */ + public int findEntry(final String path) { + final byte[] p = Constants.encode(path); + return findEntry(p, p.length); + } + + int findEntry(final byte[] p, final int pLen) { + int low = 0; + int high = entryCnt; + while (low < high) { + int mid = (low + high) >>> 1; + final int cmp = cmp(p, pLen, sortedEntries[mid]); + if (cmp < 0) + high = mid; + else if (cmp == 0) { + while (mid > 0 && cmp(p, pLen, sortedEntries[mid - 1]) == 0) + mid--; + return mid; + } else + low = mid + 1; + } + return -(low + 1); + } + + /** + * Determine the next index position past all entries with the same name. + * <p> + * As index entries are sorted by path name, then stage number, this method + * advances the supplied position to the first position in the index whose + * path name does not match the path name of the supplied position's entry. + * + * @param position + * entry position of the path that should be skipped. + * @return position of the next entry whose path is after the input. + */ + public int nextEntry(final int position) { + DirCacheEntry last = sortedEntries[position]; + int nextIdx = position + 1; + while (nextIdx < entryCnt) { + final DirCacheEntry next = sortedEntries[nextIdx]; + if (cmp(last, next) != 0) + break; + last = next; + nextIdx++; + } + return nextIdx; + } + + int nextEntry(final byte[] p, final int pLen, int nextIdx) { + while (nextIdx < entryCnt) { + final DirCacheEntry next = sortedEntries[nextIdx]; + if (!DirCacheTree.peq(p, next.path, pLen)) + break; + nextIdx++; + } + return nextIdx; + } + + /** + * Total number of file entries stored in the index. + * <p> + * This count includes unmerged stages for a file entry if the file is + * currently conflicted in a merge. This means the total number of entries + * in the index may be up to 3 times larger than the number of files in the + * working directory. + * <p> + * Note that this value counts only <i>files</i>. + * + * @return number of entries available. + * @see #getEntry(int) + */ + public int getEntryCount() { + return entryCnt; + } + + /** + * Get a specific entry. + * + * @param i + * position of the entry to get. + * @return the entry at position <code>i</code>. + */ + public DirCacheEntry getEntry(final int i) { + return sortedEntries[i]; + } + + /** + * Get a specific entry. + * + * @param path + * the path to search for. + * @return the entry at position <code>i</code>. + */ + public DirCacheEntry getEntry(final String path) { + final int i = findEntry(path); + return i < 0 ? null : sortedEntries[i]; + } + + /** + * Recursively get all entries within a subtree. + * + * @param path + * the subtree path to get all entries within. + * @return all entries recursively contained within the subtree. + */ + public DirCacheEntry[] getEntriesWithin(String path) { + if (!path.endsWith("/")) + path += "/"; + final byte[] p = Constants.encode(path); + final int pLen = p.length; + + int eIdx = findEntry(p, pLen); + if (eIdx < 0) + eIdx = -(eIdx + 1); + final int lastIdx = nextEntry(p, pLen, eIdx); + final DirCacheEntry[] r = new DirCacheEntry[lastIdx - eIdx]; + System.arraycopy(sortedEntries, eIdx, r, 0, r.length); + return r; + } + + void toArray(final int i, final DirCacheEntry[] dst, final int off, + final int cnt) { + System.arraycopy(sortedEntries, i, dst, off, cnt); + } + + /** + * Obtain (or build) the current cache tree structure. + * <p> + * This method can optionally recreate the cache tree, without flushing the + * tree objects themselves to disk. + * + * @param build + * if true and the cache tree is not present in the index it will + * be generated and returned to the caller. + * @return the cache tree; null if there is no current cache tree available + * and <code>build</code> was false. + */ + public DirCacheTree getCacheTree(final boolean build) { + if (build) { + if (tree == null) + tree = new DirCacheTree(); + tree.validate(sortedEntries, entryCnt, 0, 0); + } + return tree; + } + + /** + * Write all index trees to the object store, returning the root tree. + * + * @param ow + * the writer to use when serializing to the store. + * @return identity for the root tree. + * @throws UnmergedPathException + * one or more paths contain higher-order stages (stage > 0), + * which cannot be stored in a tree object. + * @throws IllegalStateException + * one or more paths contain an invalid mode which should never + * appear in a tree object. + * @throws IOException + * an unexpected error occurred writing to the object store. + */ + public ObjectId writeTree(final ObjectWriter ow) + throws UnmergedPathException, IOException { + return getCacheTree(true).writeTree(sortedEntries, 0, 0, ow); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java new file mode 100644 index 0000000000..ce1d0638b2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.dircache; + +import java.io.IOException; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; + +/** + * Iterate and update a {@link DirCache} as part of a <code>TreeWalk</code>. + * <p> + * Like {@link DirCacheIterator} this iterator allows a DirCache to be used in + * parallel with other sorts of iterators in a TreeWalk. However any entry which + * appears in the source DirCache and which is skipped by the TreeFilter is + * automatically copied into {@link DirCacheBuilder}, thus retaining it in the + * newly updated index. + * <p> + * This iterator is suitable for update processes, or even a simple delete + * algorithm. For example deleting a path: + * + * <pre> + * final DirCache dirc = DirCache.lock(db); + * final DirCacheBuilder edit = dirc.builder(); + * + * final TreeWalk walk = new TreeWalk(db); + * walk.reset(); + * walk.setRecursive(true); + * walk.setFilter(PathFilter.create("name/to/remove")); + * walk.addTree(new DirCacheBuildIterator(edit)); + * + * while (walk.next()) + * ; // do nothing on a match as we want to remove matches + * edit.commit(); + * </pre> + */ +public class DirCacheBuildIterator extends DirCacheIterator { + private final DirCacheBuilder builder; + + /** + * Create a new iterator for an already loaded DirCache instance. + * <p> + * The iterator implementation may copy part of the cache's data during + * construction, so the cache must be read in prior to creating the + * iterator. + * + * @param dcb + * the cache builder for the cache to walk. The cache must be + * already loaded into memory. + */ + public DirCacheBuildIterator(final DirCacheBuilder dcb) { + super(dcb.getDirCache()); + builder = dcb; + } + + DirCacheBuildIterator(final DirCacheBuildIterator p, + final DirCacheTree dct) { + super(p, dct); + builder = p.builder; + } + + @Override + public AbstractTreeIterator createSubtreeIterator(final Repository repo) + throws IncorrectObjectTypeException, IOException { + if (currentSubtree == null) + throw new IncorrectObjectTypeException(getEntryObjectId(), + Constants.TYPE_TREE); + return new DirCacheBuildIterator(this, currentSubtree); + } + + @Override + public void skip() throws CorruptObjectException { + if (currentSubtree != null) + builder.keep(ptr, currentSubtree.getEntrySpan()); + else + builder.add(currentEntry); + next(1); + } + + @Override + public void stopWalk() { + final int cur = ptr; + final int cnt = cache.getEntryCount(); + if (cur < cnt) + builder.keep(cur, cnt - cur); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuilder.java new file mode 100644 index 0000000000..f294f5cf51 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuilder.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.dircache; + +import java.io.IOException; +import java.util.Arrays; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.WindowCursor; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.TreeWalk; + +/** + * Updates a {@link DirCache} by adding individual {@link DirCacheEntry}s. + * <p> + * A builder always starts from a clean slate and appends in every single + * <code>DirCacheEntry</code> which the final updated index must have to reflect + * its new content. + * <p> + * For maximum performance applications should add entries in path name order. + * Adding entries out of order is permitted, however a final sorting pass will + * be implicitly performed during {@link #finish()} to correct any out-of-order + * entries. Duplicate detection is also delayed until the sorting is complete. + * + * @see DirCacheEditor + */ +public class DirCacheBuilder extends BaseDirCacheEditor { + private boolean sorted; + + /** + * Construct a new builder. + * + * @param dc + * the cache this builder will eventually update. + * @param ecnt + * estimated number of entries the builder will have upon + * completion. This sizes the initial entry table. + */ + protected DirCacheBuilder(final DirCache dc, final int ecnt) { + super(dc, ecnt); + } + + /** + * Append one entry into the resulting entry list. + * <p> + * The entry is placed at the end of the entry list. If the entry causes the + * list to now be incorrectly sorted a final sorting phase will be + * automatically enabled within {@link #finish()}. + * <p> + * The internal entry table is automatically expanded if there is + * insufficient space for the new addition. + * + * @param newEntry + * the new entry to add. + */ + public void add(final DirCacheEntry newEntry) { + beforeAdd(newEntry); + fastAdd(newEntry); + } + + /** + * Add a range of existing entries from the destination cache. + * <p> + * The entries are placed at the end of the entry list. If any of the + * entries causes the list to now be incorrectly sorted a final sorting + * phase will be automatically enabled within {@link #finish()}. + * <p> + * This method copies from the destination cache, which has not yet been + * updated with this editor's new table. So all offsets into the destination + * cache are not affected by any updates that may be currently taking place + * in this editor. + * <p> + * The internal entry table is automatically expanded if there is + * insufficient space for the new additions. + * + * @param pos + * first entry to copy from the destination cache. + * @param cnt + * number of entries to copy. + */ + public void keep(final int pos, int cnt) { + beforeAdd(cache.getEntry(pos)); + fastKeep(pos, cnt); + } + + /** + * Recursively add an entire tree into this builder. + * <p> + * If pathPrefix is "a/b" and the tree contains file "c" then the resulting + * DirCacheEntry will have the path "a/b/c". + * <p> + * All entries are inserted at stage 0, therefore assuming that the + * application will not insert any other paths with the same pathPrefix. + * + * @param pathPrefix + * UTF-8 encoded prefix to mount the tree's entries at. If the + * path does not end with '/' one will be automatically inserted + * as necessary. + * @param stage + * stage of the entries when adding them. + * @param db + * repository the tree(s) will be read from during recursive + * traversal. This must be the same repository that the resulting + * DirCache would be written out to (or used in) otherwise the + * caller is simply asking for deferred MissingObjectExceptions. + * @param tree + * the tree to recursively add. This tree's contents will appear + * under <code>pathPrefix</code>. The ObjectId must be that of a + * tree; the caller is responsible for dereferencing a tag or + * commit (if necessary). + * @throws IOException + * a tree cannot be read to iterate through its entries. + */ + public void addTree(final byte[] pathPrefix, final int stage, + final Repository db, final AnyObjectId tree) throws IOException { + final TreeWalk tw = new TreeWalk(db); + tw.reset(); + final WindowCursor curs = new WindowCursor(); + try { + tw.addTree(new CanonicalTreeParser(pathPrefix, db, tree + .toObjectId(), curs)); + } finally { + curs.release(); + } + tw.setRecursive(true); + if (tw.next()) { + final DirCacheEntry newEntry = toEntry(stage, tw); + beforeAdd(newEntry); + fastAdd(newEntry); + while (tw.next()) + fastAdd(toEntry(stage, tw)); + } + } + + private DirCacheEntry toEntry(final int stage, final TreeWalk tw) { + final DirCacheEntry e = new DirCacheEntry(tw.getRawPath(), stage); + final AbstractTreeIterator i; + + i = tw.getTree(0, AbstractTreeIterator.class); + e.setFileMode(tw.getFileMode(0)); + e.setObjectIdFromRaw(i.idBuffer(), i.idOffset()); + return e; + } + + public void finish() { + if (!sorted) + resort(); + replace(); + } + + private void beforeAdd(final DirCacheEntry newEntry) { + if (FileMode.TREE.equals(newEntry.getRawMode())) + throw bad(newEntry, "Adding subtree not allowed"); + if (sorted && entryCnt > 0) { + final DirCacheEntry lastEntry = entries[entryCnt - 1]; + final int cr = DirCache.cmp(lastEntry, newEntry); + if (cr > 0) { + // The new entry sorts before the old entry; we are + // no longer sorted correctly. We'll need to redo + // the sorting before we can close out the build. + // + sorted = false; + } else if (cr == 0) { + // Same file path; we can only insert this if the + // stages won't be violated. + // + final int peStage = lastEntry.getStage(); + final int dceStage = newEntry.getStage(); + if (peStage == dceStage) + throw bad(newEntry, "Duplicate stages not allowed"); + if (peStage == 0 || dceStage == 0) + throw bad(newEntry, "Mixed stages not allowed"); + if (peStage > dceStage) + sorted = false; + } + } + } + + private void resort() { + Arrays.sort(entries, 0, entryCnt, DirCache.ENT_CMP); + + for (int entryIdx = 1; entryIdx < entryCnt; entryIdx++) { + final DirCacheEntry pe = entries[entryIdx - 1]; + final DirCacheEntry ce = entries[entryIdx]; + final int cr = DirCache.cmp(pe, ce); + if (cr == 0) { + // Same file path; we can only allow this if the stages + // are 1-3 and no 0 exists. + // + final int peStage = pe.getStage(); + final int ceStage = ce.getStage(); + if (peStage == ceStage) + throw bad(ce, "Duplicate stages not allowed"); + if (peStage == 0 || ceStage == 0) + throw bad(ce, "Mixed stages not allowed"); + } + } + + sorted = true; + } + + private static IllegalStateException bad(final DirCacheEntry a, + final String msg) { + return new IllegalStateException(msg + ": " + a.getStage() + " " + + a.getPathString()); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java new file mode 100644 index 0000000000..1ad8e355d5 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.dircache; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.eclipse.jgit.lib.Constants; + +/** + * Updates a {@link DirCache} by supplying discrete edit commands. + * <p> + * An editor updates a DirCache by taking a list of {@link PathEdit} commands + * and executing them against the entries of the destination cache to produce a + * new cache. This edit style allows applications to insert a few commands and + * then have the editor compute the proper entry indexes necessary to perform an + * efficient in-order update of the index records. This can be easier to use + * than {@link DirCacheBuilder}. + * <p> + * + * @see DirCacheBuilder + */ +public class DirCacheEditor extends BaseDirCacheEditor { + private static final Comparator<PathEdit> EDIT_CMP = new Comparator<PathEdit>() { + public int compare(final PathEdit o1, final PathEdit o2) { + final byte[] a = o1.path; + final byte[] b = o2.path; + return DirCache.cmp(a, a.length, b, b.length); + } + }; + + private final List<PathEdit> edits; + + /** + * Construct a new editor. + * + * @param dc + * the cache this editor will eventually update. + * @param ecnt + * estimated number of entries the editor will have upon + * completion. This sizes the initial entry table. + */ + protected DirCacheEditor(final DirCache dc, final int ecnt) { + super(dc, ecnt); + edits = new ArrayList<PathEdit>(); + } + + /** + * Append one edit command to the list of commands to be applied. + * <p> + * Edit commands may be added in any order chosen by the application. They + * are automatically rearranged by the builder to provide the most efficient + * update possible. + * + * @param edit + * another edit command. + */ + public void add(final PathEdit edit) { + edits.add(edit); + } + + @Override + public boolean commit() throws IOException { + if (edits.isEmpty()) { + // No changes? Don't rewrite the index. + // + cache.unlock(); + return true; + } + return super.commit(); + } + + public void finish() { + if (!edits.isEmpty()) { + applyEdits(); + replace(); + } + } + + private void applyEdits() { + Collections.sort(edits, EDIT_CMP); + + final int maxIdx = cache.getEntryCount(); + int lastIdx = 0; + for (final PathEdit e : edits) { + int eIdx = cache.findEntry(e.path, e.path.length); + final boolean missing = eIdx < 0; + if (eIdx < 0) + eIdx = -(eIdx + 1); + final int cnt = Math.min(eIdx, maxIdx) - lastIdx; + if (cnt > 0) + fastKeep(lastIdx, cnt); + lastIdx = missing ? eIdx : cache.nextEntry(eIdx); + + if (e instanceof DeletePath) + continue; + if (e instanceof DeleteTree) { + lastIdx = cache.nextEntry(e.path, e.path.length, eIdx); + continue; + } + + final DirCacheEntry ent; + if (missing) + ent = new DirCacheEntry(e.path); + else + ent = cache.getEntry(eIdx); + e.apply(ent); + fastAdd(ent); + } + + final int cnt = maxIdx - lastIdx; + if (cnt > 0) + fastKeep(lastIdx, cnt); + } + + /** + * Any index record update. + * <p> + * Applications should subclass and provide their own implementation for the + * {@link #apply(DirCacheEntry)} method. The editor will invoke apply once + * for each record in the index which matches the path name. If there are + * multiple records (for example in stages 1, 2 and 3), the edit instance + * will be called multiple times, once for each stage. + */ + public abstract static class PathEdit { + final byte[] path; + + /** + * Create a new update command by path name. + * + * @param entryPath + * path of the file within the repository. + */ + public PathEdit(final String entryPath) { + path = Constants.encode(entryPath); + } + + /** + * Create a new update command for an existing entry instance. + * + * @param ent + * entry instance to match path of. Only the path of this + * entry is actually considered during command evaluation. + */ + public PathEdit(final DirCacheEntry ent) { + path = ent.path; + } + + /** + * Apply the update to a single cache entry matching the path. + * <p> + * After apply is invoked the entry is added to the output table, and + * will be included in the new index. + * + * @param ent + * the entry being processed. All fields are zeroed out if + * the path is a new path in the index. + */ + public abstract void apply(DirCacheEntry ent); + } + + /** + * Deletes a single file entry from the index. + * <p> + * This deletion command removes only a single file at the given location, + * but removes multiple stages (if present) for that path. To remove a + * complete subtree use {@link DeleteTree} instead. + * + * @see DeleteTree + */ + public static final class DeletePath extends PathEdit { + /** + * Create a new deletion command by path name. + * + * @param entryPath + * path of the file within the repository. + */ + public DeletePath(final String entryPath) { + super(entryPath); + } + + /** + * Create a new deletion command for an existing entry instance. + * + * @param ent + * entry instance to remove. Only the path of this entry is + * actually considered during command evaluation. + */ + public DeletePath(final DirCacheEntry ent) { + super(ent); + } + + public void apply(final DirCacheEntry ent) { + throw new UnsupportedOperationException("No apply in delete"); + } + } + + /** + * Recursively deletes all paths under a subtree. + * <p> + * This deletion command is more generic than {@link DeletePath} as it can + * remove all records which appear recursively under the same subtree. + * Multiple stages are removed (if present) for any deleted entry. + * <p> + * This command will not remove a single file entry. To remove a single file + * use {@link DeletePath}. + * + * @see DeletePath + */ + public static final class DeleteTree extends PathEdit { + /** + * Create a new tree deletion command by path name. + * + * @param entryPath + * path of the subtree within the repository. If the path + * does not end with "/" a "/" is implicitly added to ensure + * only the subtree's contents are matched by the command. + */ + public DeleteTree(final String entryPath) { + super(entryPath.endsWith("/") ? entryPath : entryPath + "/"); + } + + public void apply(final DirCacheEntry ent) { + throw new UnsupportedOperationException("No apply in delete"); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java new file mode 100644 index 0000000000..d3e118a550 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java @@ -0,0 +1,502 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.dircache; + +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.util.Arrays; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.util.NB; + +/** + * A single file (or stage of a file) in a {@link DirCache}. + * <p> + * An entry represents exactly one stage of a file. If a file path is unmerged + * then multiple DirCacheEntry instances may appear for the same path name. + */ +public class DirCacheEntry { + private static final byte[] nullpad = new byte[8]; + + /** The standard (fully merged) stage for an entry. */ + public static final int STAGE_0 = 0; + + /** The base tree revision for an entry. */ + public static final int STAGE_1 = 1; + + /** The first tree revision (usually called "ours"). */ + public static final int STAGE_2 = 2; + + /** The second tree revision (usually called "theirs"). */ + public static final int STAGE_3 = 3; + + // private static final int P_CTIME = 0; + + // private static final int P_CTIME_NSEC = 4; + + private static final int P_MTIME = 8; + + // private static final int P_MTIME_NSEC = 12; + + // private static final int P_DEV = 16; + + // private static final int P_INO = 20; + + private static final int P_MODE = 24; + + // private static final int P_UID = 28; + + // private static final int P_GID = 32; + + private static final int P_SIZE = 36; + + private static final int P_OBJECTID = 40; + + private static final int P_FLAGS = 60; + + /** Mask applied to data in {@link #P_FLAGS} to get the name length. */ + private static final int NAME_MASK = 0xfff; + + static final int INFO_LEN = 62; + + private static final int ASSUME_VALID = 0x80; + + /** (Possibly shared) header information storage. */ + private final byte[] info; + + /** First location within {@link #info} where our header starts. */ + private final int infoOffset; + + /** Our encoded path name, from the root of the repository. */ + final byte[] path; + + DirCacheEntry(final byte[] sharedInfo, final int infoAt, + final InputStream in, final MessageDigest md) throws IOException { + info = sharedInfo; + infoOffset = infoAt; + + NB.readFully(in, info, infoOffset, INFO_LEN); + md.update(info, infoOffset, INFO_LEN); + + int pathLen = NB.decodeUInt16(info, infoOffset + P_FLAGS) & NAME_MASK; + int skipped = 0; + if (pathLen < NAME_MASK) { + path = new byte[pathLen]; + NB.readFully(in, path, 0, pathLen); + md.update(path, 0, pathLen); + } else { + final ByteArrayOutputStream tmp = new ByteArrayOutputStream(); + { + final byte[] buf = new byte[NAME_MASK]; + NB.readFully(in, buf, 0, NAME_MASK); + tmp.write(buf); + } + for (;;) { + final int c = in.read(); + if (c < 0) + throw new EOFException("Short read of block."); + if (c == 0) + break; + tmp.write(c); + } + path = tmp.toByteArray(); + pathLen = path.length; + skipped = 1; // we already skipped 1 '\0' above to break the loop. + md.update(path, 0, pathLen); + md.update((byte) 0); + } + + // Index records are padded out to the next 8 byte alignment + // for historical reasons related to how C Git read the files. + // + final int actLen = INFO_LEN + pathLen; + final int expLen = (actLen + 8) & ~7; + final int padLen = expLen - actLen - skipped; + if (padLen > 0) { + NB.skipFully(in, padLen); + md.update(nullpad, 0, padLen); + } + } + + /** + * Create an empty entry at stage 0. + * + * @param newPath + * name of the cache entry. + */ + public DirCacheEntry(final String newPath) { + this(Constants.encode(newPath)); + } + + /** + * Create an empty entry at the specified stage. + * + * @param newPath + * name of the cache entry. + * @param stage + * the stage index of the new entry. + */ + public DirCacheEntry(final String newPath, final int stage) { + this(Constants.encode(newPath), stage); + } + + /** + * Create an empty entry at stage 0. + * + * @param newPath + * name of the cache entry, in the standard encoding. + */ + public DirCacheEntry(final byte[] newPath) { + this(newPath, STAGE_0); + } + + /** + * Create an empty entry at the specified stage. + * + * @param newPath + * name of the cache entry, in the standard encoding. + * @param stage + * the stage index of the new entry. + */ + public DirCacheEntry(final byte[] newPath, final int stage) { + info = new byte[INFO_LEN]; + infoOffset = 0; + path = newPath; + + int flags = ((stage & 0x3) << 12); + if (path.length < NAME_MASK) + flags |= path.length; + else + flags |= NAME_MASK; + NB.encodeInt16(info, infoOffset + P_FLAGS, flags); + } + + void write(final OutputStream os) throws IOException { + final int pathLen = path.length; + os.write(info, infoOffset, INFO_LEN); + os.write(path, 0, pathLen); + + // Index records are padded out to the next 8 byte alignment + // for historical reasons related to how C Git read the files. + // + final int actLen = INFO_LEN + pathLen; + final int expLen = (actLen + 8) & ~7; + if (actLen != expLen) + os.write(nullpad, 0, expLen - actLen); + } + + /** + * Is it possible for this entry to be accidentally assumed clean? + * <p> + * The "racy git" problem happens when a work file can be updated faster + * than the filesystem records file modification timestamps. It is possible + * for an application to edit a work file, update the index, then edit it + * again before the filesystem will give the work file a new modification + * timestamp. This method tests to see if file was written out at the same + * time as the index. + * + * @param smudge_s + * seconds component of the index's last modified time. + * @param smudge_ns + * nanoseconds component of the index's last modified time. + * @return true if extra careful checks should be used. + */ + final boolean mightBeRacilyClean(final int smudge_s, final int smudge_ns) { + // If the index has a modification time then it came from disk + // and was not generated from scratch in memory. In such cases + // the entry is 'racily clean' if the entry's cached modification + // time is equal to or later than the index modification time. In + // such cases the work file is too close to the index to tell if + // it is clean or not based on the modification time alone. + // + final int base = infoOffset + P_MTIME; + final int mtime = NB.decodeInt32(info, base); + if (smudge_s < mtime) + return true; + if (smudge_s == mtime) + return smudge_ns <= NB.decodeInt32(info, base + 4) / 1000000; + return false; + } + + /** + * Force this entry to no longer match its working tree file. + * <p> + * This avoids the "racy git" problem by making this index entry no longer + * match the file in the working directory. Later git will be forced to + * compare the file content to ensure the file matches the working tree. + */ + final void smudgeRacilyClean() { + // We don't use the same approach as C Git to smudge the entry, + // as we cannot compare the working tree file to our SHA-1 and + // thus cannot use the "size to 0" trick without accidentally + // thinking a zero length file is clean. + // + // Instead we force the mtime to the largest possible value, so + // it is certainly after the index's own modification time and + // on a future read will cause mightBeRacilyClean to say "yes!". + // It is also unlikely to match with the working tree file. + // + // I'll see you again before Jan 19, 2038, 03:14:07 AM GMT. + // + final int base = infoOffset + P_MTIME; + Arrays.fill(info, base, base + 8, (byte) 127); + } + + final byte[] idBuffer() { + return info; + } + + final int idOffset() { + return infoOffset + P_OBJECTID; + } + + /** + * Is this entry always thought to be unmodified? + * <p> + * Most entries in the index do not have this flag set. Users may however + * set them on if the file system stat() costs are too high on this working + * directory, such as on NFS or SMB volumes. + * + * @return true if we must assume the entry is unmodified. + */ + public boolean isAssumeValid() { + return (info[infoOffset + P_FLAGS] & ASSUME_VALID) != 0; + } + + /** + * Set the assume valid flag for this entry, + * + * @param assume + * true to ignore apparent modifications; false to look at last + * modified to detect file modifications. + */ + public void setAssumeValid(final boolean assume) { + if (assume) + info[infoOffset + P_FLAGS] |= ASSUME_VALID; + else + info[infoOffset + P_FLAGS] &= ~ASSUME_VALID; + } + + /** + * Get the stage of this entry. + * <p> + * Entries have one of 4 possible stages: 0-3. + * + * @return the stage of this entry. + */ + public int getStage() { + return (info[infoOffset + P_FLAGS] >>> 4) & 0x3; + } + + /** + * Obtain the raw {@link FileMode} bits for this entry. + * + * @return mode bits for the entry. + * @see FileMode#fromBits(int) + */ + public int getRawMode() { + return NB.decodeInt32(info, infoOffset + P_MODE); + } + + /** + * Obtain the {@link FileMode} for this entry. + * + * @return the file mode singleton for this entry. + */ + public FileMode getFileMode() { + return FileMode.fromBits(getRawMode()); + } + + /** + * Set the file mode for this entry. + * + * @param mode + * the new mode constant. + */ + public void setFileMode(final FileMode mode) { + NB.encodeInt32(info, infoOffset + P_MODE, mode.getBits()); + } + + /** + * Get the cached last modification date of this file, in milliseconds. + * <p> + * One of the indicators that the file has been modified by an application + * changing the working tree is if the last modification time for the file + * differs from the time stored in this entry. + * + * @return last modification time of this file, in milliseconds since the + * Java epoch (midnight Jan 1, 1970 UTC). + */ + public long getLastModified() { + return decodeTS(P_MTIME); + } + + /** + * Set the cached last modification date of this file, using milliseconds. + * + * @param when + * new cached modification date of the file, in milliseconds. + */ + public void setLastModified(final long when) { + encodeTS(P_MTIME, when); + } + + /** + * Get the cached size (in bytes) of this file. + * <p> + * One of the indicators that the file has been modified by an application + * changing the working tree is if the size of the file (in bytes) differs + * from the size stored in this entry. + * <p> + * Note that this is the length of the file in the working directory, which + * may differ from the size of the decompressed blob if work tree filters + * are being used, such as LF<->CRLF conversion. + * + * @return cached size of the working directory file, in bytes. + */ + public int getLength() { + return NB.decodeInt32(info, infoOffset + P_SIZE); + } + + /** + * Set the cached size (in bytes) of this file. + * + * @param sz + * new cached size of the file, as bytes. + */ + public void setLength(final int sz) { + NB.encodeInt32(info, infoOffset + P_SIZE, sz); + } + + /** + * Obtain the ObjectId for the entry. + * <p> + * Using this method to compare ObjectId values between entries is + * inefficient as it causes memory allocation. + * + * @return object identifier for the entry. + */ + public ObjectId getObjectId() { + return ObjectId.fromRaw(idBuffer(), idOffset()); + } + + /** + * Set the ObjectId for the entry. + * + * @param id + * new object identifier for the entry. May be + * {@link ObjectId#zeroId()} to remove the current identifier. + */ + public void setObjectId(final AnyObjectId id) { + id.copyRawTo(idBuffer(), idOffset()); + } + + /** + * Set the ObjectId for the entry from the raw binary representation. + * + * @param bs + * the raw byte buffer to read from. At least 20 bytes after p + * must be available within this byte array. + * @param p + * position to read the first byte of data from. + */ + public void setObjectIdFromRaw(final byte[] bs, final int p) { + final int n = Constants.OBJECT_ID_LENGTH; + System.arraycopy(bs, p, idBuffer(), idOffset(), n); + } + + /** + * Get the entry's complete path. + * <p> + * This method is not very efficient and is primarily meant for debugging + * and final output generation. Applications should try to avoid calling it, + * and if invoked do so only once per interesting entry, where the name is + * absolutely required for correct function. + * + * @return complete path of the entry, from the root of the repository. If + * the entry is in a subtree there will be at least one '/' in the + * returned string. + */ + public String getPathString() { + return Constants.CHARSET.decode(ByteBuffer.wrap(path)).toString(); + } + + /** + * Copy the ObjectId and other meta fields from an existing entry. + * <p> + * This method copies everything except the path from one entry to another, + * supporting renaming. + * + * @param src + * the entry to copy ObjectId and meta fields from. + */ + public void copyMetaData(final DirCacheEntry src) { + final int pLen = NB.decodeUInt16(info, infoOffset + P_FLAGS) & NAME_MASK; + System.arraycopy(src.info, src.infoOffset, info, infoOffset, INFO_LEN); + NB.encodeInt16(info, infoOffset + P_FLAGS, pLen + | NB.decodeUInt16(info, infoOffset + P_FLAGS) & ~NAME_MASK); + } + + private long decodeTS(final int pIdx) { + final int base = infoOffset + pIdx; + final int sec = NB.decodeInt32(info, base); + final int ms = NB.decodeInt32(info, base + 4) / 1000000; + return 1000L * sec + ms; + } + + private void encodeTS(final int pIdx, final long when) { + final int base = infoOffset + pIdx; + NB.encodeInt32(info, base, (int) (when / 1000)); + NB.encodeInt32(info, base + 4, ((int) (when % 1000)) * 1000000); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheIterator.java new file mode 100644 index 0000000000..9c47187821 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheIterator.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.dircache; + +import java.io.IOException; +import java.util.Arrays; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; +import org.eclipse.jgit.treewalk.EmptyTreeIterator; + +/** + * Iterate a {@link DirCache} as part of a <code>TreeWalk</code>. + * <p> + * This is an iterator to adapt a loaded <code>DirCache</code> instance (such as + * read from an existing <code>.git/index</code> file) to the tree structure + * used by a <code>TreeWalk</code>, making it possible for applications to walk + * over any combination of tree objects already in the object database, index + * files, or working directories. + * + * @see org.eclipse.jgit.treewalk.TreeWalk + */ +public class DirCacheIterator extends AbstractTreeIterator { + /** The cache this iterator was created to walk. */ + protected final DirCache cache; + + /** The tree this iterator is walking. */ + private final DirCacheTree tree; + + /** First position in this tree. */ + private final int treeStart; + + /** Last position in this tree. */ + private final int treeEnd; + + /** Special buffer to hold the ObjectId of {@link #currentSubtree}. */ + private final byte[] subtreeId; + + /** Index of entry within {@link #cache}. */ + protected int ptr; + + /** Next subtree to consider within {@link #tree}. */ + private int nextSubtreePos; + + /** The current file entry from {@link #cache}. */ + protected DirCacheEntry currentEntry; + + /** The subtree containing {@link #currentEntry} if this is first entry. */ + protected DirCacheTree currentSubtree; + + /** + * Create a new iterator for an already loaded DirCache instance. + * <p> + * The iterator implementation may copy part of the cache's data during + * construction, so the cache must be read in prior to creating the + * iterator. + * + * @param dc + * the cache to walk. It must be already loaded into memory. + */ + public DirCacheIterator(final DirCache dc) { + cache = dc; + tree = dc.getCacheTree(true); + treeStart = 0; + treeEnd = tree.getEntrySpan(); + subtreeId = new byte[Constants.OBJECT_ID_LENGTH]; + if (!eof()) + parseEntry(); + } + + DirCacheIterator(final DirCacheIterator p, final DirCacheTree dct) { + super(p, p.path, p.pathLen + 1); + cache = p.cache; + tree = dct; + treeStart = p.ptr; + treeEnd = treeStart + tree.getEntrySpan(); + subtreeId = p.subtreeId; + ptr = p.ptr; + parseEntry(); + } + + @Override + public AbstractTreeIterator createSubtreeIterator(final Repository repo) + throws IncorrectObjectTypeException, IOException { + if (currentSubtree == null) + throw new IncorrectObjectTypeException(getEntryObjectId(), + Constants.TYPE_TREE); + return new DirCacheIterator(this, currentSubtree); + } + + @Override + public EmptyTreeIterator createEmptyTreeIterator() { + final byte[] n = new byte[Math.max(pathLen + 1, DEFAULT_PATH_SIZE)]; + System.arraycopy(path, 0, n, 0, pathLen); + n[pathLen] = '/'; + return new EmptyTreeIterator(this, n, pathLen + 1); + } + + @Override + public byte[] idBuffer() { + if (currentSubtree != null) + return subtreeId; + if (currentEntry != null) + return currentEntry.idBuffer(); + return zeroid; + } + + @Override + public int idOffset() { + if (currentSubtree != null) + return 0; + if (currentEntry != null) + return currentEntry.idOffset(); + return 0; + } + + @Override + public boolean first() { + return ptr == treeStart; + } + + @Override + public boolean eof() { + return ptr == treeEnd; + } + + @Override + public void next(int delta) { + while (--delta >= 0) { + if (currentSubtree != null) + ptr += currentSubtree.getEntrySpan(); + else + ptr++; + if (eof()) + break; + parseEntry(); + } + } + + @Override + public void back(int delta) { + while (--delta >= 0) { + if (currentSubtree != null) + nextSubtreePos--; + ptr--; + parseEntry(); + if (currentSubtree != null) + ptr -= currentSubtree.getEntrySpan() - 1; + } + } + + private void parseEntry() { + currentEntry = cache.getEntry(ptr); + final byte[] cep = currentEntry.path; + + if (nextSubtreePos != tree.getChildCount()) { + final DirCacheTree s = tree.getChild(nextSubtreePos); + if (s.contains(cep, pathOffset, cep.length)) { + // The current position is the first file of this subtree. + // Use the subtree instead as the current position. + // + currentSubtree = s; + nextSubtreePos++; + + if (s.isValid()) + s.getObjectId().copyRawTo(subtreeId, 0); + else + Arrays.fill(subtreeId, (byte) 0); + mode = FileMode.TREE.getBits(); + path = cep; + pathLen = pathOffset + s.nameLength(); + return; + } + } + + // The current position is a file/symlink/gitlink so we + // do not have a subtree located here. + // + mode = currentEntry.getRawMode(); + path = cep; + pathLen = cep.length; + currentSubtree = null; + } + + /** + * Get the DirCacheEntry for the current file. + * + * @return the current cache entry, if this iterator is positioned on a + * non-tree. + */ + public DirCacheEntry getDirCacheEntry() { + return currentSubtree == null ? currentEntry : null; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java new file mode 100644 index 0000000000..fc29aa71b8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java @@ -0,0 +1,575 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.dircache; + +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Comparator; + +import org.eclipse.jgit.errors.UnmergedPathException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectWriter; +import org.eclipse.jgit.util.MutableInteger; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * Single tree record from the 'TREE' {@link DirCache} extension. + * <p> + * A valid cache tree record contains the object id of a tree object and the + * total number of {@link DirCacheEntry} instances (counted recursively) from + * the DirCache contained within the tree. This information facilitates faster + * traversal of the index and quicker generation of tree objects prior to + * creating a new commit. + * <p> + * An invalid cache tree record indicates a known subtree whose file entries + * have changed in ways that cause the tree to no longer have a known object id. + * Invalid cache tree records must be revalidated prior to use. + */ +public class DirCacheTree { + private static final byte[] NO_NAME = {}; + + private static final DirCacheTree[] NO_CHILDREN = {}; + + private static final Comparator<DirCacheTree> TREE_CMP = new Comparator<DirCacheTree>() { + public int compare(final DirCacheTree o1, final DirCacheTree o2) { + final byte[] a = o1.encodedName; + final byte[] b = o2.encodedName; + final int aLen = a.length; + final int bLen = b.length; + int cPos; + for (cPos = 0; cPos < aLen && cPos < bLen; cPos++) { + final int cmp = (a[cPos] & 0xff) - (b[cPos] & 0xff); + if (cmp != 0) + return cmp; + } + if (aLen == bLen) + return 0; + if (aLen < bLen) + return '/' - (b[cPos] & 0xff); + return (a[cPos] & 0xff) - '/'; + } + }; + + /** Tree this tree resides in; null if we are the root. */ + private DirCacheTree parent; + + /** Name of this tree within its parent. */ + private byte[] encodedName; + + /** Number of {@link DirCacheEntry} records that belong to this tree. */ + private int entrySpan; + + /** Unique SHA-1 of this tree; null if invalid. */ + private ObjectId id; + + /** Child trees, if any, sorted by {@link #encodedName}. */ + private DirCacheTree[] children; + + /** Number of valid children in {@link #children}. */ + private int childCnt; + + DirCacheTree() { + encodedName = NO_NAME; + children = NO_CHILDREN; + childCnt = 0; + entrySpan = -1; + } + + private DirCacheTree(final DirCacheTree myParent, final byte[] path, + final int pathOff, final int pathLen) { + parent = myParent; + encodedName = new byte[pathLen]; + System.arraycopy(path, pathOff, encodedName, 0, pathLen); + children = NO_CHILDREN; + childCnt = 0; + entrySpan = -1; + } + + DirCacheTree(final byte[] in, final MutableInteger off, + final DirCacheTree myParent) { + parent = myParent; + + int ptr = RawParseUtils.next(in, off.value, '\0'); + final int nameLen = ptr - off.value - 1; + if (nameLen > 0) { + encodedName = new byte[nameLen]; + System.arraycopy(in, off.value, encodedName, 0, nameLen); + } else + encodedName = NO_NAME; + + entrySpan = RawParseUtils.parseBase10(in, ptr, off); + final int subcnt = RawParseUtils.parseBase10(in, off.value, off); + off.value = RawParseUtils.next(in, off.value, '\n'); + + if (entrySpan >= 0) { + // Valid trees have a positive entry count and an id of a + // tree object that should exist in the object database. + // + id = ObjectId.fromRaw(in, off.value); + off.value += Constants.OBJECT_ID_LENGTH; + } + + if (subcnt > 0) { + boolean alreadySorted = true; + children = new DirCacheTree[subcnt]; + for (int i = 0; i < subcnt; i++) { + children[i] = new DirCacheTree(in, off, this); + + // C Git's ordering differs from our own; it prefers to + // sort by length first. This sometimes produces a sort + // we do not desire. On the other hand it may have been + // created by us, and be sorted the way we want. + // + if (alreadySorted && i > 0 + && TREE_CMP.compare(children[i - 1], children[i]) > 0) + alreadySorted = false; + } + if (!alreadySorted) + Arrays.sort(children, 0, subcnt, TREE_CMP); + } else { + // Leaf level trees have no children, only (file) entries. + // + children = NO_CHILDREN; + } + childCnt = subcnt; + } + + void write(final byte[] tmp, final OutputStream os) throws IOException { + int ptr = tmp.length; + tmp[--ptr] = '\n'; + ptr = RawParseUtils.formatBase10(tmp, ptr, childCnt); + tmp[--ptr] = ' '; + ptr = RawParseUtils.formatBase10(tmp, ptr, isValid() ? entrySpan : -1); + tmp[--ptr] = 0; + + os.write(encodedName); + os.write(tmp, ptr, tmp.length - ptr); + if (isValid()) { + id.copyRawTo(tmp, 0); + os.write(tmp, 0, Constants.OBJECT_ID_LENGTH); + } + for (int i = 0; i < childCnt; i++) + children[i].write(tmp, os); + } + + /** + * Determine if this cache is currently valid. + * <p> + * A valid cache tree knows how many {@link DirCacheEntry} instances from + * the parent {@link DirCache} reside within this tree (recursively + * enumerated). It also knows the object id of the tree, as the tree should + * be readily available from the repository's object database. + * + * @return true if this tree is knows key details about itself; false if the + * tree needs to be regenerated. + */ + public boolean isValid() { + return id != null; + } + + /** + * Get the number of entries this tree spans within the DirCache. + * <p> + * If this tree is not valid (see {@link #isValid()}) this method's return + * value is always strictly negative (less than 0) but is otherwise an + * undefined result. + * + * @return total number of entries (recursively) contained within this tree. + */ + public int getEntrySpan() { + return entrySpan; + } + + /** + * Get the number of cached subtrees contained within this tree. + * + * @return number of child trees available through this tree. + */ + public int getChildCount() { + return childCnt; + } + + /** + * Get the i-th child cache tree. + * + * @param i + * index of the child to obtain. + * @return the child tree. + */ + public DirCacheTree getChild(final int i) { + return children[i]; + } + + ObjectId getObjectId() { + return id; + } + + /** + * Get the tree's name within its parent. + * <p> + * This method is not very efficient and is primarily meant for debugging + * and final output generation. Applications should try to avoid calling it, + * and if invoked do so only once per interesting entry, where the name is + * absolutely required for correct function. + * + * @return name of the tree. This does not contain any '/' characters. + */ + public String getNameString() { + final ByteBuffer bb = ByteBuffer.wrap(encodedName); + return Constants.CHARSET.decode(bb).toString(); + } + + /** + * Get the tree's path within the repository. + * <p> + * This method is not very efficient and is primarily meant for debugging + * and final output generation. Applications should try to avoid calling it, + * and if invoked do so only once per interesting entry, where the name is + * absolutely required for correct function. + * + * @return path of the tree, relative to the repository root. If this is not + * the root tree the path ends with '/'. The root tree's path string + * is the empty string (""). + */ + public String getPathString() { + final StringBuilder r = new StringBuilder(); + appendName(r); + return r.toString(); + } + + /** + * Write (if necessary) this tree to the object store. + * + * @param cache + * the complete cache from DirCache. + * @param cIdx + * first position of <code>cache</code> that is a member of this + * tree. The path of <code>cache[cacheIdx].path</code> for the + * range <code>[0,pathOff-1)</code> matches the complete path of + * this tree, from the root of the repository. + * @param pathOffset + * number of bytes of <code>cache[cacheIdx].path</code> that + * matches this tree's path. The value at array position + * <code>cache[cacheIdx].path[pathOff-1]</code> is always '/' if + * <code>pathOff</code> is > 0. + * @param ow + * the writer to use when serializing to the store. + * @return identity of this tree. + * @throws UnmergedPathException + * one or more paths contain higher-order stages (stage > 0), + * which cannot be stored in a tree object. + * @throws IOException + * an unexpected error occurred writing to the object store. + */ + ObjectId writeTree(final DirCacheEntry[] cache, int cIdx, + final int pathOffset, final ObjectWriter ow) + throws UnmergedPathException, IOException { + if (id == null) { + final int endIdx = cIdx + entrySpan; + final int size = computeSize(cache, cIdx, pathOffset, ow); + final ByteArrayOutputStream out = new ByteArrayOutputStream(size); + int childIdx = 0; + int entryIdx = cIdx; + + while (entryIdx < endIdx) { + final DirCacheEntry e = cache[entryIdx]; + final byte[] ep = e.path; + if (childIdx < childCnt) { + final DirCacheTree st = children[childIdx]; + if (st.contains(ep, pathOffset, ep.length)) { + FileMode.TREE.copyTo(out); + out.write(' '); + out.write(st.encodedName); + out.write(0); + st.id.copyRawTo(out); + + entryIdx += st.entrySpan; + childIdx++; + continue; + } + } + + e.getFileMode().copyTo(out); + out.write(' '); + out.write(ep, pathOffset, ep.length - pathOffset); + out.write(0); + out.write(e.idBuffer(), e.idOffset(), OBJECT_ID_LENGTH); + entryIdx++; + } + + id = ow.writeCanonicalTree(out.toByteArray()); + } + return id; + } + + private int computeSize(final DirCacheEntry[] cache, int cIdx, + final int pathOffset, final ObjectWriter ow) + throws UnmergedPathException, IOException { + final int endIdx = cIdx + entrySpan; + int childIdx = 0; + int entryIdx = cIdx; + int size = 0; + + while (entryIdx < endIdx) { + final DirCacheEntry e = cache[entryIdx]; + if (e.getStage() != 0) + throw new UnmergedPathException(e); + + final byte[] ep = e.path; + if (childIdx < childCnt) { + final DirCacheTree st = children[childIdx]; + if (st.contains(ep, pathOffset, ep.length)) { + final int stOffset = pathOffset + st.nameLength() + 1; + st.writeTree(cache, entryIdx, stOffset, ow); + + size += FileMode.TREE.copyToLength(); + size += st.nameLength(); + size += OBJECT_ID_LENGTH + 2; + + entryIdx += st.entrySpan; + childIdx++; + continue; + } + } + + final FileMode mode = e.getFileMode(); + if (mode.getObjectType() == Constants.OBJ_BAD) + throw new IllegalStateException("Entry \"" + e.getPathString() + + "\" has incorrect mode set up."); + + size += mode.copyToLength(); + size += ep.length - pathOffset; + size += OBJECT_ID_LENGTH + 2; + entryIdx++; + } + + return size; + } + + private void appendName(final StringBuilder r) { + if (parent != null) { + parent.appendName(r); + r.append(getNameString()); + r.append('/'); + } else if (nameLength() > 0) { + r.append(getNameString()); + r.append('/'); + } + } + + final int nameLength() { + return encodedName.length; + } + + final boolean contains(final byte[] a, int aOff, final int aLen) { + final byte[] e = encodedName; + final int eLen = e.length; + for (int eOff = 0; eOff < eLen && aOff < aLen; eOff++, aOff++) + if (e[eOff] != a[aOff]) + return false; + if (aOff == aLen) + return false; + return a[aOff] == '/'; + } + + /** + * Update (if necessary) this tree's entrySpan. + * + * @param cache + * the complete cache from DirCache. + * @param cCnt + * number of entries in <code>cache</code> that are valid for + * iteration. + * @param cIdx + * first position of <code>cache</code> that is a member of this + * tree. The path of <code>cache[cacheIdx].path</code> for the + * range <code>[0,pathOff-1)</code> matches the complete path of + * this tree, from the root of the repository. + * @param pathOff + * number of bytes of <code>cache[cacheIdx].path</code> that + * matches this tree's path. The value at array position + * <code>cache[cacheIdx].path[pathOff-1]</code> is always '/' if + * <code>pathOff</code> is > 0. + */ + void validate(final DirCacheEntry[] cache, final int cCnt, int cIdx, + final int pathOff) { + if (entrySpan >= 0) { + // If we are valid, our children are also valid. + // We have no need to validate them. + // + return; + } + + entrySpan = 0; + if (cCnt == 0) { + // Special case of an empty index, and we are the root tree. + // + return; + } + + final byte[] firstPath = cache[cIdx].path; + int stIdx = 0; + while (cIdx < cCnt) { + final byte[] currPath = cache[cIdx].path; + if (pathOff > 0 && !peq(firstPath, currPath, pathOff)) { + // The current entry is no longer in this tree. Our + // span is updated and the remainder goes elsewhere. + // + break; + } + + DirCacheTree st = stIdx < childCnt ? children[stIdx] : null; + final int cc = namecmp(currPath, pathOff, st); + if (cc > 0) { + // This subtree is now empty. + // + removeChild(stIdx); + continue; + } + + if (cc < 0) { + final int p = slash(currPath, pathOff); + if (p < 0) { + // The entry has no '/' and thus is directly in this + // tree. Count it as one of our own. + // + cIdx++; + entrySpan++; + continue; + } + + // Build a new subtree for this entry. + // + st = new DirCacheTree(this, currPath, pathOff, p - pathOff); + insertChild(stIdx, st); + } + + // The entry is contained in this subtree. + // + st.validate(cache, cCnt, cIdx, pathOff + st.nameLength() + 1); + cIdx += st.entrySpan; + entrySpan += st.entrySpan; + stIdx++; + } + + if (stIdx < childCnt) { + // None of our remaining children can be in this tree + // as the current cache entry is after our own name. + // + final DirCacheTree[] dct = new DirCacheTree[stIdx]; + System.arraycopy(children, 0, dct, 0, stIdx); + children = dct; + } + } + + private void insertChild(final int stIdx, final DirCacheTree st) { + final DirCacheTree[] c = children; + if (childCnt + 1 <= c.length) { + if (stIdx < childCnt) + System.arraycopy(c, stIdx, c, stIdx + 1, childCnt - stIdx); + c[stIdx] = st; + childCnt++; + return; + } + + final int n = c.length; + final DirCacheTree[] a = new DirCacheTree[n + 1]; + if (stIdx > 0) + System.arraycopy(c, 0, a, 0, stIdx); + a[stIdx] = st; + if (stIdx < n) + System.arraycopy(c, stIdx, a, stIdx + 1, n - stIdx); + children = a; + childCnt++; + } + + private void removeChild(final int stIdx) { + final int n = --childCnt; + if (stIdx < n) + System.arraycopy(children, stIdx + 1, children, stIdx, n - stIdx); + children[n] = null; + } + + static boolean peq(final byte[] a, final byte[] b, int aLen) { + if (b.length < aLen) + return false; + for (aLen--; aLen >= 0; aLen--) + if (a[aLen] != b[aLen]) + return false; + return true; + } + + private static int namecmp(final byte[] a, int aPos, final DirCacheTree ct) { + if (ct == null) + return -1; + final byte[] b = ct.encodedName; + final int aLen = a.length; + final int bLen = b.length; + int bPos = 0; + for (; aPos < aLen && bPos < bLen; aPos++, bPos++) { + final int cmp = (a[aPos] & 0xff) - (b[bPos] & 0xff); + if (cmp != 0) + return cmp; + } + if (bPos == bLen) + return a[aPos] == '/' ? 0 : -1; + return aLen - bLen; + } + + private static int slash(final byte[] a, int aPos) { + final int aLen = a.length; + for (; aPos < aLen; aPos++) + if (a[aPos] == '/') + return aPos; + return -1; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/CheckoutConflictException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CheckoutConflictException.java new file mode 100644 index 0000000000..8cfc366ea0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CheckoutConflictException.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.IOException; + +/** + * Exception thrown if a conflict occurs during a merge checkout. + */ +public class CheckoutConflictException extends IOException { + private static final long serialVersionUID = 1L; + + /** + * Construct a CheckoutConflictException for the specified file + * + * @param file + */ + public CheckoutConflictException(String file) { + super("Checkout conflict with file: " + file); + } + + /** + * Construct a CheckoutConflictException for the specified set of files + * + * @param files + */ + public CheckoutConflictException(String[] files) { + super("Checkout conflict with files: " + buildList(files)); + } + + private static String buildList(String[] files) { + StringBuilder builder = new StringBuilder(); + for (String f : files) { + builder.append("\n"); + builder.append(f); + } + return builder.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/CompoundException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CompoundException.java new file mode 100644 index 0000000000..0fb14655f0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CompoundException.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** An exception detailing multiple reasons for failure. */ +public class CompoundException extends Exception { + private static final long serialVersionUID = 1L; + + private static String format(final Collection<Throwable> causes) { + final StringBuilder msg = new StringBuilder(); + msg.append("Failure due to one of the following:"); + for (final Throwable c : causes) { + msg.append(" "); + msg.append(c.getMessage()); + msg.append("\n"); + } + return msg.toString(); + } + + private final List<Throwable> causeList; + + /** + * Constructs an exception detailing many potential reasons for failure. + * + * @param why + * Two or more exceptions that may have been the problem. + */ + public CompoundException(final Collection<Throwable> why) { + super(format(why)); + causeList = Collections.unmodifiableList(new ArrayList<Throwable>(why)); + } + + /** + * Get the complete list of reasons why this failure happened. + * + * @return unmodifiable collection of all possible reasons. + */ + public List<Throwable> getAllCauses() { + return causeList; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/ConfigInvalidException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/ConfigInvalidException.java new file mode 100644 index 0000000000..43fb4bcf8b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/ConfigInvalidException.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +/** Indicates a text string is not a valid Git style configuration. */ +public class ConfigInvalidException extends Exception { + private static final long serialVersionUID = 1L; + + /** + * Construct an invalid configuration error. + * + * @param message + * why the configuration is invalid. + */ + public ConfigInvalidException(final String message) { + super(message); + } + + /** + * Construct an invalid configuration error. + * + * @param message + * why the configuration is invalid. + * @param cause + * root cause of the error. + */ + public ConfigInvalidException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java new file mode 100644 index 0000000000..f42b0d7e19 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008, Jonas Fonseca <fonseca@diku.dk> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.IOException; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; + +/** + * Exception thrown when an object cannot be read from Git. + */ +public class CorruptObjectException extends IOException { + private static final long serialVersionUID = 1L; + + /** + * Construct a CorruptObjectException for reporting a problem specified + * object id + * + * @param id + * @param why + */ + public CorruptObjectException(final AnyObjectId id, final String why) { + this(id.toObjectId(), why); + } + + /** + * Construct a CorruptObjectException for reporting a problem specified + * object id + * + * @param id + * @param why + */ + public CorruptObjectException(final ObjectId id, final String why) { + super("Object " + id.name() + " is corrupt: " + why); + } + + /** + * Construct a CorruptObjectException for reporting a problem not associated + * with a specific object id. + * + * @param why + */ + public CorruptObjectException(final String why) { + super(why); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/EntryExistsException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/EntryExistsException.java new file mode 100644 index 0000000000..893ee9ceb2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/EntryExistsException.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.IOException; + +/** + * Attempt to add an entry to a tree that already exists. + */ +public class EntryExistsException extends IOException { + private static final long serialVersionUID = 1L; + + /** + * Construct an EntryExistsException when the specified name already + * exists in a tree. + * + * @param name workdir relative file name + */ + public EntryExistsException(final String name) { + super("Tree entry \"" + name + "\" already exists."); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/GitlinksNotSupportedException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/GitlinksNotSupportedException.java new file mode 100644 index 0000000000..d501bda38b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/GitlinksNotSupportedException.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2009, Jonas Fonseca <fonseca@diku.dk> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.IOException; + +/** + * An exception thrown when a gitlink entry is found and cannot be + * handled. + */ +public class GitlinksNotSupportedException extends IOException { + private static final long serialVersionUID = 1L; + + /** + * Construct a GitlinksNotSupportedException for the specified link + * + * @param s name of link in tree or workdir + */ + public GitlinksNotSupportedException(final String s) { + super(s); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/IncorrectObjectTypeException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/IncorrectObjectTypeException.java new file mode 100644 index 0000000000..7cf1de214f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/IncorrectObjectTypeException.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Jonas Fonseca <fonseca@diku.dk> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.IOException; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; + +/** + * An inconsistency with respect to handling different object types. + * + * This most likely signals a programming error rather than a corrupt + * object database. + */ +public class IncorrectObjectTypeException extends IOException { + private static final long serialVersionUID = 1L; + + /** + * Construct and IncorrectObjectTypeException for the specified object id. + * + * Provide the type to make it easier to track down the problem. + * + * @param id SHA-1 + * @param type object type + */ + public IncorrectObjectTypeException(final ObjectId id, final String type) { + super("Object " + id.name() + " is not a " + type + "."); + } + + /** + * Construct and IncorrectObjectTypeException for the specified object id. + * + * Provide the type to make it easier to track down the problem. + * + * @param id SHA-1 + * @param type object type + */ + public IncorrectObjectTypeException(final ObjectId id, final int type) { + this(id, Constants.typeString(type)); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidObjectIdException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidObjectIdException.java new file mode 100644 index 0000000000..e6577213ee --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidObjectIdException.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2009, Jonas Fonseca <fonseca@diku.dk> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.UnsupportedEncodingException; + +/** + * Thrown when an invalid object id is passed in as an argument. + */ +public class InvalidObjectIdException extends IllegalArgumentException { + private static final long serialVersionUID = 1L; + + /** + * Create exception with bytes of the invalid object id. + * + * @param bytes containing the invalid id. + * @param offset in the byte array where the error occurred. + * @param length of the sequence of invalid bytes. + */ + public InvalidObjectIdException(byte[] bytes, int offset, int length) { + super("Invalid id" + asAscii(bytes, offset, length)); + } + + private static String asAscii(byte[] bytes, int offset, int length) { + try { + return ": " + new String(bytes, offset, length, "US-ASCII"); + } catch (UnsupportedEncodingException e2) { + return ""; + } catch (StringIndexOutOfBoundsException e2) { + return ""; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidPatternException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidPatternException.java new file mode 100644 index 0000000000..18e78ffe76 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidPatternException.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2008, Florian Koeberle <florianskarten@web.de> + * Copyright (C) 2008, Florian Köberle <florianskarten@web.de> + * Copyright (C) 2009, Vasyl' Vavrychuk <vvavrychuk@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +/** + * Thrown when a pattern passed in an argument was wrong. + * + */ +public class InvalidPatternException extends Exception { + private static final long serialVersionUID = 1L; + + private final String pattern; + + /** + * @param message + * explains what was wrong with the pattern. + * @param pattern + * the invalid pattern. + */ + public InvalidPatternException(String message, String pattern) { + super(message); + this.pattern = pattern; + } + + /** + * @return the invalid pattern. + */ + public String getPattern() { + return pattern; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/MissingBundlePrerequisiteException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/MissingBundlePrerequisiteException.java new file mode 100644 index 0000000000..92d63d2993 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/MissingBundlePrerequisiteException.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.util.Collection; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.transport.URIish; + +/** + * Indicates a base/common object was required, but is not found. + */ +public class MissingBundlePrerequisiteException extends TransportException { + private static final long serialVersionUID = 1L; + + private static String format(final Collection<ObjectId> ids) { + final StringBuilder r = new StringBuilder(); + r.append("missing prerequisite commits:"); + for (final ObjectId p : ids) { + r.append("\n "); + r.append(p.name()); + } + return r.toString(); + } + + /** + * Constructs a MissingBundlePrerequisiteException for a set of objects. + * + * @param uri + * URI used for transport + * @param ids + * the ids of the base/common object(s) we don't have. + */ + public MissingBundlePrerequisiteException(final URIish uri, + final Collection<ObjectId> ids) { + super(uri, format(ids)); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/MissingObjectException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/MissingObjectException.java new file mode 100644 index 0000000000..41cacb84ad --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/MissingObjectException.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Jonas Fonseca <fonseca@diku.dk> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.IOException; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; + +/** + * An expected object is missing. + */ +public class MissingObjectException extends IOException { + private static final long serialVersionUID = 1L; + + /** + * Construct a MissingObjectException for the specified object id. + * Expected type is reported to simplify tracking down the problem. + * + * @param id SHA-1 + * @param type object type + */ + public MissingObjectException(final ObjectId id, final String type) { + super("Missing " + type + " " + id.name()); + } + + /** + * Construct a MissingObjectException for the specified object id. + * Expected type is reported to simplify tracking down the problem. + * + * @param id SHA-1 + * @param type object type + */ + public MissingObjectException(final ObjectId id, final int type) { + this(id, Constants.typeString(type)); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoClosingBracketException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoClosingBracketException.java new file mode 100644 index 0000000000..623dfa6ec6 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoClosingBracketException.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2008, Florian Koeberle <florianskarten@web.de> + * Copyright (C) 2008, Florian Köberle <florianskarten@web.de> + * Copyright (C) 2009, Vasyl' Vavrychuk <vvavrychuk@gmail.com> + * Copyright (C) 2009, Yann Simon <yann.simon.fr@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +/** + * Thrown when a pattern contains a character group which is open to the right + * side or a character class which is open to the right side. + */ +public class NoClosingBracketException extends InvalidPatternException { + private static final long serialVersionUID = 1L; + + /** + * @param indexOfOpeningBracket + * the position of the [ character which has no ] character. + * @param openingBracket + * the unclosed bracket. + * @param closingBracket + * the missing closing bracket. + * @param pattern + * the invalid pattern. + */ + public NoClosingBracketException(final int indexOfOpeningBracket, + final String openingBracket, final String closingBracket, + final String pattern) { + super(createMessage(indexOfOpeningBracket, openingBracket, + closingBracket), pattern); + } + + private static String createMessage(final int indexOfOpeningBracket, + final String openingBracket, final String closingBracket) { + return String.format("No closing %s found for %s at index %s.", + closingBracket, openingBracket, + Integer.valueOf(indexOfOpeningBracket)); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoRemoteRepositoryException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoRemoteRepositoryException.java new file mode 100644 index 0000000000..fe073d5648 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoRemoteRepositoryException.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import org.eclipse.jgit.transport.URIish; + +/** + * Indicates a remote repository does not exist. + */ +public class NoRemoteRepositoryException extends TransportException { + private static final long serialVersionUID = 1L; + + /** + * Constructs an exception indicating a repository does not exist. + * + * @param uri + * URI used for transport + * @param s + * message + */ + public NoRemoteRepositoryException(final URIish uri, final String s) { + super(uri, s); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/NotSupportedException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NotSupportedException.java new file mode 100644 index 0000000000..87044cbf3d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NotSupportedException.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.IOException; + +/** + * JGit encountered a case that it knows it cannot yet handle. + */ +public class NotSupportedException extends IOException { + private static final long serialVersionUID = 1L; + + /** + * Construct a NotSupportedException for some issue JGit cannot + * yet handle. + * + * @param s message describing the issue + */ + public NotSupportedException(final String s) { + super(s); + } + + /** + * Construct a NotSupportedException for some issue JGit cannot yet handle. + * + * @param s + * message describing the issue + * @param why + * a lower level implementation specific issue. + */ + public NotSupportedException(final String s, final Throwable why) { + super(s); + initCause(why); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/ObjectWritingException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/ObjectWritingException.java new file mode 100644 index 0000000000..8af1991b14 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/ObjectWritingException.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.IOException; + +/** + * Cannot store an object in the object database. This is a serious + * error that users need to be made aware of. + */ +public class ObjectWritingException extends IOException { + private static final long serialVersionUID = 1L; + + /** + * Constructs an ObjectWritingException with the specified detail message. + * + * @param s message + */ + public ObjectWritingException(final String s) { + super(s); + } + + /** + * Constructs an ObjectWritingException with the specified detail message. + * + * @param s message + * @param cause root cause exception + */ + public ObjectWritingException(final String s, final Throwable cause) { + super(s); + initCause(cause); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackInvalidException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackInvalidException.java new file mode 100644 index 0000000000..a34b80db81 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackInvalidException.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.File; +import java.io.IOException; + +/** Thrown when a PackFile previously failed and is known to be unusable */ +public class PackInvalidException extends IOException { + private static final long serialVersionUID = 1L; + + /** + * Construct a pack invalid error. + * + * @param path + * path of the invalid pack file. + */ + public PackInvalidException(final File path) { + super("Pack file invalid: " + path.getAbsolutePath()); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackMismatchException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackMismatchException.java new file mode 100644 index 0000000000..b82846530d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackMismatchException.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.IOException; + +/** Thrown when a PackFile no longer matches the PackIndex. */ +public class PackMismatchException extends IOException { + private static final long serialVersionUID = 1L; + + /** + * Construct a pack modification error. + * + * @param why + * description of the type of error. + */ + public PackMismatchException(final String why) { + super(why); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackProtocolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackProtocolException.java new file mode 100644 index 0000000000..f9a1bae937 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackProtocolException.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import org.eclipse.jgit.transport.URIish; + +/** + * Indicates a protocol error has occurred while fetching/pushing objects. + */ +public class PackProtocolException extends TransportException { + private static final long serialVersionUID = 1L; + + /** + * Constructs an PackProtocolException with the specified detail message + * prefixed with provided URI. + * + * @param uri + * URI used for transport + * @param s + * message + */ + public PackProtocolException(final URIish uri, final String s) { + super(uri + ": " + s); + } + + /** + * Constructs an PackProtocolException with the specified detail message + * prefixed with provided URI. + * + * @param uri + * URI used for transport + * @param s + * message + * @param cause + * root cause exception + */ + public PackProtocolException(final URIish uri, final String s, + final Throwable cause) { + this(uri + ": " + s, cause); + } + + /** + * Constructs an PackProtocolException with the specified detail message. + * + * @param s + * message + */ + public PackProtocolException(final String s) { + super(s); + } + + /** + * Constructs an PackProtocolException with the specified detail message. + * + * @param s + * message + * @param cause + * root cause exception + */ + public PackProtocolException(final String s, final Throwable cause) { + super(s); + initCause(cause); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/RepositoryNotFoundException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/RepositoryNotFoundException.java new file mode 100644 index 0000000000..d947a2cdc7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/RepositoryNotFoundException.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.File; + +/** Indicates a local repository does not exist. */ +public class RepositoryNotFoundException extends TransportException { + private static final long serialVersionUID = 1L; + + /** + * Constructs an exception indicating a local repository does not exist. + * + * @param location + * description of the repository not found, usually file path. + */ + public RepositoryNotFoundException(final File location) { + this(location.getPath()); + } + + /** + * Constructs an exception indicating a local repository does not exist. + * + * @param location + * description of the repository not found, usually file path. + */ + public RepositoryNotFoundException(final String location) { + super("repository not found: " + location); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/RevWalkException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/RevWalkException.java new file mode 100644 index 0000000000..0ad41ed173 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/RevWalkException.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Indicates a checked exception was thrown inside of {@link RevWalk}. + * <p> + * Usually this exception is thrown from the Iterator created around a RevWalk + * instance, as the Iterator API does not allow checked exceptions to be thrown + * from hasNext() or next(). The {@link Exception#getCause()} of this exception + * is the original checked exception that we really wanted to throw back to the + * application for handling and recovery. + */ +public class RevWalkException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * Create a new walk exception an original cause. + * + * @param cause + * the checked exception that describes why the walk failed. + */ + public RevWalkException(final Throwable cause) { + super("Walk failure.", cause); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/RevisionSyntaxException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/RevisionSyntaxException.java new file mode 100644 index 0000000000..58ec1b0233 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/RevisionSyntaxException.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org> + * Copyright (C) 2009, Vasyl' Vavrychuk <vvavrychuk@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.IOException; + +/** + * This signals a revision or object reference was not + * properly formatted. + */ +public class RevisionSyntaxException extends IOException { + private static final long serialVersionUID = 1L; + + private final String revstr; + + /** + * Construct a RevisionSyntaxException indicating a syntax problem with a + * revision (or object) string. + * + * @param revstr The problematic revision string + */ + public RevisionSyntaxException(String revstr) { + this.revstr = revstr; + } + + /** + * Construct a RevisionSyntaxException indicating a syntax problem with a + * revision (or object) string. + * + * @param message a specific reason + * @param revstr The problematic revision string + */ + public RevisionSyntaxException(String message, String revstr) { + super(message); + this.revstr = revstr; + } + + @Override + public String toString() { + return super.toString() + ":" + revstr; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/StopWalkException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/StopWalkException.java new file mode 100644 index 0000000000..c9b51de36f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/StopWalkException.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +/** + * Stops the driver loop of walker and finish with current results. + * + * @see org.eclipse.jgit.revwalk.filter.RevFilter + */ +public class StopWalkException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** Singleton instance for throwing within a filter. */ + public static final StopWalkException INSTANCE = new StopWalkException(); + + private StopWalkException() { + // Nothing. + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/SymlinksNotSupportedException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/SymlinksNotSupportedException.java new file mode 100644 index 0000000000..60d7aa098f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/SymlinksNotSupportedException.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.IOException; + +/** + * An exception thrown when a symlink entry is found and cannot be + * handled. + */ +public class SymlinksNotSupportedException extends IOException { + private static final long serialVersionUID = 1L; + + /** + * Construct a SymlinksNotSupportedException for the specified link + * + * @param s name of link in tree or workdir + */ + public SymlinksNotSupportedException(final String s) { + super(s); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/TransportException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/TransportException.java new file mode 100644 index 0000000000..2856eb62cc --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/TransportException.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.IOException; + +import org.eclipse.jgit.transport.URIish; + +/** + * Indicates a protocol error has occurred while fetching/pushing objects. + */ +public class TransportException extends IOException { + private static final long serialVersionUID = 1L; + + /** + * Constructs an TransportException with the specified detail message + * prefixed with provided URI. + * + * @param uri + * URI used for transport + * @param s + * message + */ + public TransportException(final URIish uri, final String s) { + super(uri.setPass(null) + ": " + s); + } + + /** + * Constructs an TransportException with the specified detail message + * prefixed with provided URI. + * + * @param uri + * URI used for transport + * @param s + * message + * @param cause + * root cause exception + */ + public TransportException(final URIish uri, final String s, + final Throwable cause) { + this(uri.setPass(null) + ": " + s, cause); + } + + /** + * Constructs an TransportException with the specified detail message. + * + * @param s + * message + */ + public TransportException(final String s) { + super(s); + } + + /** + * Constructs an TransportException with the specified detail message. + * + * @param s + * message + * @param cause + * root cause exception + */ + public TransportException(final String s, final Throwable cause) { + super(s); + initCause(cause); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/UnmergedPathException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/UnmergedPathException.java new file mode 100644 index 0000000000..51e651ca5e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/UnmergedPathException.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.errors; + +import java.io.IOException; + +import org.eclipse.jgit.dircache.DirCacheEntry; + +/** + * Indicates one or more paths in a DirCache have non-zero stages present. + */ +public class UnmergedPathException extends IOException { + private static final long serialVersionUID = 1L; + + private final DirCacheEntry entry; + + /** + * Create a new unmerged path exception. + * + * @param dce + * the first non-zero stage of the unmerged path. + */ + public UnmergedPathException(final DirCacheEntry dce) { + super("Unmerged path: " + dce.getPathString()); + entry = dce; + } + + /** @return the first non-zero stage of the unmerged path */ + public DirCacheEntry getDirCacheEntry() { + return entry; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/AbstractHead.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/AbstractHead.java new file mode 100644 index 0000000000..42182965a0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/AbstractHead.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2008, Florian Koeberle <florianskarten@web.de> + * Copyright (C) 2008, Florian Köberle <florianskarten@web.de> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.fnmatch; + +import java.util.List; + +abstract class AbstractHead implements Head { + private List<Head> newHeads = null; + + private final boolean star; + + protected abstract boolean matches(char c); + + AbstractHead(boolean star) { + this.star = star; + } + + /** + * + * @param newHeads + * a list of {@link Head}s which will not be modified. + */ + public final void setNewHeads(List<Head> newHeads) { + if (this.newHeads != null) + throw new IllegalStateException("Property is already non null"); + this.newHeads = newHeads; + } + + public List<Head> getNextHeads(char c) { + if (matches(c)) + return newHeads; + else + return FileNameMatcher.EMPTY_HEAD_LIST; + } + + boolean isStar() { + return star; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/CharacterHead.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/CharacterHead.java new file mode 100644 index 0000000000..699eca9683 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/CharacterHead.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2008, Florian Koeberle <florianskarten@web.de> + * Copyright (C) 2008, Florian Köberle <florianskarten@web.de> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.fnmatch; + +final class CharacterHead extends AbstractHead { + private final char expectedCharacter; + + protected CharacterHead(final char expectedCharacter) { + super(false); + this.expectedCharacter = expectedCharacter; + } + + @Override + protected final boolean matches(final char c) { + return c == expectedCharacter; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/FileNameMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/FileNameMatcher.java new file mode 100644 index 0000000000..582123bb5b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/FileNameMatcher.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2008, Florian Koeberle <florianskarten@web.de> + * Copyright (C) 2008, Florian Köberle <florianskarten@web.de> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.fnmatch; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jgit.errors.InvalidPatternException; +import org.eclipse.jgit.errors.NoClosingBracketException; + +/** + * This class can be used to match filenames against fnmatch like patterns. It + * is not thread save. + * <p> + * Supported are the wildcard characters * and ? and groups with: + * <ul> + * <li> characters e.g. [abc]</li> + * <li> ranges e.g. [a-z]</li> + * <li> the following character classes + * <ul> + * <li>[:alnum:]</li> + * <li>[:alpha:]</li> + * <li>[:blank:]</li> + * <li>[:cntrl:]</li> + * <li>[:digit:]</li> + * <li>[:graph:]</li> + * <li>[:lower:]</li> + * <li>[:print:]</li> + * <li>[:punct:]</li> + * <li>[:space:]</li> + * <li>[:upper:]</li> + * <li>[:word:]</li> + * <li>[:xdigit:]</li> + * </ul> + * e. g. [[:xdigit:]] </li> + * </ul> + * </p> + */ +public class FileNameMatcher { + static final List<Head> EMPTY_HEAD_LIST = Collections.emptyList(); + + private static final Pattern characterClassStartPattern = Pattern + .compile("\\[[.:=]"); + + private List<Head> headsStartValue; + + private List<Head> heads; + + /** + * {{@link #extendStringToMatchByOneCharacter(char)} needs a list for the + * new heads, allocating a new array would be bad for the performance, as + * the method gets called very often. + * + */ + private List<Head> listForLocalUseage; + + /** + * + * @param headsStartValue + * must be a list which will never be modified. + */ + private FileNameMatcher(final List<Head> headsStartValue) { + this(headsStartValue, headsStartValue); + } + + /** + * + * @param headsStartValue + * must be a list which will never be modified. + * @param heads + * a list which will be cloned and then used as current head + * list. + */ + private FileNameMatcher(final List<Head> headsStartValue, + final List<Head> heads) { + this.headsStartValue = headsStartValue; + this.heads = new ArrayList<Head>(heads.size()); + this.heads.addAll(heads); + this.listForLocalUseage = new ArrayList<Head>(heads.size()); + } + + /** + * @param patternString + * must contain a pattern which fnmatch would accept. + * @param invalidWildgetCharacter + * if this parameter isn't null then this character will not + * match at wildcards(* and ? are wildcards). + * @throws InvalidPatternException + * if the patternString contains a invalid fnmatch pattern. + */ + public FileNameMatcher(final String patternString, + final Character invalidWildgetCharacter) + throws InvalidPatternException { + this(createHeadsStartValues(patternString, invalidWildgetCharacter)); + } + + /** + * A Copy Constructor which creates a new {@link FileNameMatcher} with the + * same state and reset point like <code>other</code>. + * + * @param other + * another {@link FileNameMatcher} instance. + */ + public FileNameMatcher(FileNameMatcher other) { + this(other.headsStartValue, other.heads); + } + + private static List<Head> createHeadsStartValues( + final String patternString, final Character invalidWildgetCharacter) + throws InvalidPatternException { + + final List<AbstractHead> allHeads = parseHeads(patternString, + invalidWildgetCharacter); + + List<Head> nextHeadsSuggestion = new ArrayList<Head>(2); + nextHeadsSuggestion.add(LastHead.INSTANCE); + for (int i = allHeads.size() - 1; i >= 0; i--) { + final AbstractHead head = allHeads.get(i); + + // explanation: + // a and * of the pattern "a*b" + // need *b as newHeads + // that's why * extends the list for it self and it's left neighbor. + if (head.isStar()) { + nextHeadsSuggestion.add(head); + head.setNewHeads(nextHeadsSuggestion); + } else { + head.setNewHeads(nextHeadsSuggestion); + nextHeadsSuggestion = new ArrayList<Head>(2); + nextHeadsSuggestion.add(head); + } + } + return nextHeadsSuggestion; + } + + private static int findGroupEnd(final int indexOfStartBracket, + final String pattern) throws InvalidPatternException { + int firstValidCharClassIndex = indexOfStartBracket + 1; + int firstValidEndBracketIndex = indexOfStartBracket + 2; + + if (indexOfStartBracket + 1 >= pattern.length()) + throw new NoClosingBracketException(indexOfStartBracket, "[", "]", + pattern); + + if (pattern.charAt(firstValidCharClassIndex) == '!') { + firstValidCharClassIndex++; + firstValidEndBracketIndex++; + } + + final Matcher charClassStartMatcher = characterClassStartPattern + .matcher(pattern); + + int groupEnd = -1; + while (groupEnd == -1) { + + final int possibleGroupEnd = pattern.indexOf(']', + firstValidEndBracketIndex); + if (possibleGroupEnd == -1) + throw new NoClosingBracketException(indexOfStartBracket, "[", + "]", pattern); + + final boolean foundCharClass = charClassStartMatcher + .find(firstValidCharClassIndex); + + if (foundCharClass + && charClassStartMatcher.start() < possibleGroupEnd) { + + final String classStart = charClassStartMatcher.group(0); + final String classEnd = classStart.charAt(1) + "]"; + + final int classStartIndex = charClassStartMatcher.start(); + final int classEndIndex = pattern.indexOf(classEnd, + classStartIndex + 2); + + if (classEndIndex == -1) + throw new NoClosingBracketException(classStartIndex, + classStart, classEnd, pattern); + + firstValidCharClassIndex = classEndIndex + 2; + firstValidEndBracketIndex = firstValidCharClassIndex; + } else { + groupEnd = possibleGroupEnd; + } + } + return groupEnd; + } + + private static List<AbstractHead> parseHeads(final String pattern, + final Character invalidWildgetCharacter) + throws InvalidPatternException { + + int currentIndex = 0; + List<AbstractHead> heads = new ArrayList<AbstractHead>(); + while (currentIndex < pattern.length()) { + final int groupStart = pattern.indexOf('[', currentIndex); + if (groupStart == -1) { + final String patternPart = pattern.substring(currentIndex); + heads.addAll(createSimpleHeads(patternPart, + invalidWildgetCharacter)); + currentIndex = pattern.length(); + } else { + final String patternPart = pattern.substring(currentIndex, + groupStart); + heads.addAll(createSimpleHeads(patternPart, + invalidWildgetCharacter)); + + final int groupEnd = findGroupEnd(groupStart, pattern); + final String groupPart = pattern.substring(groupStart + 1, + groupEnd); + heads.add(new GroupHead(groupPart, pattern)); + currentIndex = groupEnd + 1; + } + } + return heads; + } + + private static List<AbstractHead> createSimpleHeads( + final String patternPart, final Character invalidWildgetCharacter) { + final List<AbstractHead> heads = new ArrayList<AbstractHead>( + patternPart.length()); + for (int i = 0; i < patternPart.length(); i++) { + final char c = patternPart.charAt(i); + switch (c) { + case '*': { + final AbstractHead head = createWildCardHead( + invalidWildgetCharacter, true); + heads.add(head); + break; + } + case '?': { + final AbstractHead head = createWildCardHead( + invalidWildgetCharacter, false); + heads.add(head); + break; + } + default: + final CharacterHead head = new CharacterHead(c); + heads.add(head); + } + } + return heads; + } + + private static AbstractHead createWildCardHead( + final Character invalidWildgetCharacter, final boolean star) { + if (invalidWildgetCharacter != null) + return new RestrictedWildCardHead(invalidWildgetCharacter + .charValue(), star); + else + return new WildCardHead(star); + } + + private void extendStringToMatchByOneCharacter(final char c) { + final List<Head> newHeads = listForLocalUseage; + newHeads.clear(); + List<Head> lastAddedHeads = null; + for (int i = 0; i < heads.size(); i++) { + final Head head = heads.get(i); + final List<Head> headsToAdd = head.getNextHeads(c); + // Why the next performance optimization isn't wrong: + // Some times two heads return the very same list. + // We save future effort if we don't add these heads again. + // This is the case with the heads "a" and "*" of "a*b" which + // both can return the list ["*","b"] + if (headsToAdd != lastAddedHeads) { + newHeads.addAll(headsToAdd); + lastAddedHeads = headsToAdd; + } + } + listForLocalUseage = heads; + heads = newHeads; + } + + /** + * + * @param stringToMatch + * extends the string which is matched against the patterns of + * this class. + */ + public void append(final String stringToMatch) { + for (int i = 0; i < stringToMatch.length(); i++) { + final char c = stringToMatch.charAt(i); + extendStringToMatchByOneCharacter(c); + } + } + + /** + * Resets this matcher to it's state right after construction. + */ + public void reset() { + heads.clear(); + heads.addAll(headsStartValue); + } + + /** + * + * @return a {@link FileNameMatcher} instance which uses the same pattern + * like this matcher, but has the current state of this matcher as + * reset and start point. + */ + public FileNameMatcher createMatcherForSuffix() { + final List<Head> copyOfHeads = new ArrayList<Head>(heads.size()); + copyOfHeads.addAll(heads); + return new FileNameMatcher(copyOfHeads); + } + + /** + * + * @return true, if the string currently being matched does match. + */ + public boolean isMatch() { + final ListIterator<Head> headIterator = heads + .listIterator(heads.size()); + while (headIterator.hasPrevious()) { + final Head head = headIterator.previous(); + if (head == LastHead.INSTANCE) { + return true; + } + } + return false; + } + + /** + * + * @return false, if the string being matched will not match when the string + * gets extended. + */ + public boolean canAppendMatch() { + for (int i = 0; i < heads.size(); i++) { + if (heads.get(i) != LastHead.INSTANCE) { + return true; + } + } + return false; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/GroupHead.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/GroupHead.java new file mode 100644 index 0000000000..79f64f859e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/GroupHead.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2008, Florian Koeberle <florianskarten@web.de> + * Copyright (C) 2008, Florian Köberle <florianskarten@web.de> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.fnmatch; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jgit.errors.InvalidPatternException; + +final class GroupHead extends AbstractHead { + private final List<CharacterPattern> characterClasses; + + private static final Pattern REGEX_PATTERN = Pattern + .compile("([^-][-][^-]|\\[[.:=].*?[.:=]\\])"); + + private final boolean inverse; + + GroupHead(String pattern, final String wholePattern) + throws InvalidPatternException { + super(false); + this.characterClasses = new ArrayList<CharacterPattern>(); + this.inverse = pattern.startsWith("!"); + if (inverse) { + pattern = pattern.substring(1); + } + final Matcher matcher = REGEX_PATTERN.matcher(pattern); + while (matcher.find()) { + final String characterClass = matcher.group(0); + if (characterClass.length() == 3 && characterClass.charAt(1) == '-') { + final char start = characterClass.charAt(0); + final char end = characterClass.charAt(2); + characterClasses.add(new CharacterRange(start, end)); + } else if (characterClass.equals("[:alnum:]")) { + characterClasses.add(LetterPattern.INSTANCE); + characterClasses.add(DigitPattern.INSTANCE); + } else if (characterClass.equals("[:alpha:]")) { + characterClasses.add(LetterPattern.INSTANCE); + } else if (characterClass.equals("[:blank:]")) { + characterClasses.add(new OneCharacterPattern(' ')); + characterClasses.add(new OneCharacterPattern('\t')); + } else if (characterClass.equals("[:cntrl:]")) { + characterClasses.add(new CharacterRange('\u0000', '\u001F')); + characterClasses.add(new OneCharacterPattern('\u007F')); + } else if (characterClass.equals("[:digit:]")) { + characterClasses.add(DigitPattern.INSTANCE); + } else if (characterClass.equals("[:graph:]")) { + characterClasses.add(new CharacterRange('\u0021', '\u007E')); + characterClasses.add(LetterPattern.INSTANCE); + characterClasses.add(DigitPattern.INSTANCE); + } else if (characterClass.equals("[:lower:]")) { + characterClasses.add(LowerPattern.INSTANCE); + } else if (characterClass.equals("[:print:]")) { + characterClasses.add(new CharacterRange('\u0020', '\u007E')); + characterClasses.add(LetterPattern.INSTANCE); + characterClasses.add(DigitPattern.INSTANCE); + } else if (characterClass.equals("[:punct:]")) { + characterClasses.add(PunctPattern.INSTANCE); + } else if (characterClass.equals("[:space:]")) { + characterClasses.add(WhitespacePattern.INSTANCE); + } else if (characterClass.equals("[:upper:]")) { + characterClasses.add(UpperPattern.INSTANCE); + } else if (characterClass.equals("[:xdigit:]")) { + characterClasses.add(new CharacterRange('0', '9')); + characterClasses.add(new CharacterRange('a', 'f')); + characterClasses.add(new CharacterRange('A', 'F')); + } else if (characterClass.equals("[:word:]")) { + characterClasses.add(new OneCharacterPattern('_')); + characterClasses.add(LetterPattern.INSTANCE); + characterClasses.add(DigitPattern.INSTANCE); + } else { + final String message = String.format( + "The character class %s is not supported.", + characterClass); + throw new InvalidPatternException(message, wholePattern); + } + + pattern = matcher.replaceFirst(""); + matcher.reset(pattern); + } + // pattern contains now no ranges + for (int i = 0; i < pattern.length(); i++) { + final char c = pattern.charAt(i); + characterClasses.add(new OneCharacterPattern(c)); + } + } + + @Override + protected final boolean matches(final char c) { + for (CharacterPattern pattern : characterClasses) { + if (pattern.matches(c)) { + return !inverse; + } + } + return inverse; + } + + private interface CharacterPattern { + /** + * @param c + * the character to test + * @return returns true if the character matches a pattern. + */ + boolean matches(char c); + } + + private static final class CharacterRange implements CharacterPattern { + private final char start; + + private final char end; + + CharacterRange(char start, char end) { + this.start = start; + this.end = end; + } + + public final boolean matches(char c) { + return start <= c && c <= end; + } + } + + private static final class DigitPattern implements CharacterPattern { + static final GroupHead.DigitPattern INSTANCE = new DigitPattern(); + + public final boolean matches(char c) { + return Character.isDigit(c); + } + } + + private static final class LetterPattern implements CharacterPattern { + static final GroupHead.LetterPattern INSTANCE = new LetterPattern(); + + public final boolean matches(char c) { + return Character.isLetter(c); + } + } + + private static final class LowerPattern implements CharacterPattern { + static final GroupHead.LowerPattern INSTANCE = new LowerPattern(); + + public final boolean matches(char c) { + return Character.isLowerCase(c); + } + } + + private static final class UpperPattern implements CharacterPattern { + static final GroupHead.UpperPattern INSTANCE = new UpperPattern(); + + public final boolean matches(char c) { + return Character.isUpperCase(c); + } + } + + private static final class WhitespacePattern implements CharacterPattern { + static final GroupHead.WhitespacePattern INSTANCE = new WhitespacePattern(); + + public final boolean matches(char c) { + return Character.isWhitespace(c); + } + } + + private static final class OneCharacterPattern implements CharacterPattern { + private char expectedCharacter; + + OneCharacterPattern(final char c) { + this.expectedCharacter = c; + } + + public final boolean matches(char c) { + return this.expectedCharacter == c; + } + } + + private static final class PunctPattern implements CharacterPattern { + static final GroupHead.PunctPattern INSTANCE = new PunctPattern(); + + private static String punctCharacters = "-!\"#$%&'()*+,./:;<=>?@[\\]_`{|}~"; + + public boolean matches(char c) { + return punctCharacters.indexOf(c) != -1; + } + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/Head.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/Head.java new file mode 100644 index 0000000000..3de18a7357 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/Head.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2008, Florian Koeberle <florianskarten@web.de> + * Copyright (C) 2008, Florian Köberle <florianskarten@web.de> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.fnmatch; + +import java.util.List; + +interface Head { + /** + * + * @param c + * the character which decides which heads are returned. + * @return a list of heads based on the input. + */ + public abstract List<Head> getNextHeads(char c); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/LastHead.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/LastHead.java new file mode 100644 index 0000000000..78a61b9c51 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/LastHead.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2008, Florian Koeberle <florianskarten@web.de> + * Copyright (C) 2008, Florian Köberle <florianskarten@web.de> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.fnmatch; + +import java.util.List; + +final class LastHead implements Head { + static final Head INSTANCE = new LastHead(); + + /** + * Don't call this constructor, use {@link #INSTANCE} + */ + private LastHead() { + // defined because of javadoc and visibility modifier. + } + + public List<Head> getNextHeads(char c) { + return FileNameMatcher.EMPTY_HEAD_LIST; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/RestrictedWildCardHead.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/RestrictedWildCardHead.java new file mode 100644 index 0000000000..6d527d2b2d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/RestrictedWildCardHead.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2008, Florian Koeberle <florianskarten@web.de> + * Copyright (C) 2008, Florian Köberle <florianskarten@web.de> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.fnmatch; + +final class RestrictedWildCardHead extends AbstractHead { + private final char excludedCharacter; + + RestrictedWildCardHead(final char excludedCharacter, final boolean star) { + super(star); + this.excludedCharacter = excludedCharacter; + } + + @Override + protected final boolean matches(final char c) { + return c != excludedCharacter; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/WildCardHead.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/WildCardHead.java new file mode 100644 index 0000000000..b5173d97d0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/WildCardHead.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2008, Florian Koeberle <florianskarten@web.de> + * Copyright (C) 2008, Florian Köberle <florianskarten@web.de> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.fnmatch; + +final class WildCardHead extends AbstractHead { + WildCardHead(boolean star) { + super(star); + } + + @Override + protected final boolean matches(final char c) { + return true; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java new file mode 100644 index 0000000000..13c54201c6 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import org.eclipse.jgit.errors.InvalidObjectIdException; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * A prefix abbreviation of an {@link ObjectId}. + * <p> + * Sometimes Git produces abbreviated SHA-1 strings, using sufficient leading + * digits from the ObjectId name to still be unique within the repository the + * string was generated from. These ids are likely to be unique for a useful + * period of time, especially if they contain at least 6-10 hex digits. + * <p> + * This class converts the hex string into a binary form, to make it more + * efficient for matching against an object. + */ +public final class AbbreviatedObjectId { + /** + * Convert an AbbreviatedObjectId from hex characters (US-ASCII). + * + * @param buf + * the US-ASCII buffer to read from. + * @param offset + * position to read the first character from. + * @param end + * one past the last position to read (<code>end-offset</code> is + * the length of the string). + * @return the converted object id. + */ + public static final AbbreviatedObjectId fromString(final byte[] buf, + final int offset, final int end) { + if (end - offset > AnyObjectId.STR_LEN) + throw new IllegalArgumentException("Invalid id"); + return fromHexString(buf, offset, end); + } + + /** + * Convert an AbbreviatedObjectId from hex characters. + * + * @param str + * the string to read from. Must be <= 40 characters. + * @return the converted object id. + */ + public static final AbbreviatedObjectId fromString(final String str) { + if (str.length() > AnyObjectId.STR_LEN) + throw new IllegalArgumentException("Invalid id: " + str); + final byte[] b = Constants.encodeASCII(str); + return fromHexString(b, 0, b.length); + } + + private static final AbbreviatedObjectId fromHexString(final byte[] bs, + int ptr, final int end) { + try { + final int a = hexUInt32(bs, ptr, end); + final int b = hexUInt32(bs, ptr + 8, end); + final int c = hexUInt32(bs, ptr + 16, end); + final int d = hexUInt32(bs, ptr + 24, end); + final int e = hexUInt32(bs, ptr + 32, end); + return new AbbreviatedObjectId(end - ptr, a, b, c, d, e); + } catch (ArrayIndexOutOfBoundsException e1) { + throw new InvalidObjectIdException(bs, ptr, end - ptr); + } + } + + private static final int hexUInt32(final byte[] bs, int p, final int end) { + if (8 <= end - p) + return RawParseUtils.parseHexInt32(bs, p); + + int r = 0, n = 0; + while (n < 8 && p < end) { + r <<= 4; + r |= RawParseUtils.parseHexInt4(bs[p++]); + n++; + } + return r << (8 - n) * 4; + } + + static int mask(final int nibbles, final int word, final int v) { + final int b = (word - 1) * 8; + if (b + 8 <= nibbles) { + // We have all of the bits required for this word. + // + return v; + } + + if (nibbles <= b) { + // We have none of the bits required for this word. + // + return 0; + } + + final int s = 32 - (nibbles - b) * 4; + return (v >>> s) << s; + } + + /** Number of half-bytes used by this id. */ + final int nibbles; + + final int w1; + + final int w2; + + final int w3; + + final int w4; + + final int w5; + + AbbreviatedObjectId(final int n, final int new_1, final int new_2, + final int new_3, final int new_4, final int new_5) { + nibbles = n; + w1 = new_1; + w2 = new_2; + w3 = new_3; + w4 = new_4; + w5 = new_5; + } + + /** @return number of hex digits appearing in this id */ + public int length() { + return nibbles; + } + + /** @return true if this ObjectId is actually a complete id. */ + public boolean isComplete() { + return length() == AnyObjectId.RAW_LEN * 2; + } + + /** @return a complete ObjectId; null if {@link #isComplete()} is false */ + public ObjectId toObjectId() { + return isComplete() ? new ObjectId(w1, w2, w3, w4, w5) : null; + } + + /** + * Compares this abbreviation to a full object id. + * + * @param other + * the other object id. + * @return <0 if this abbreviation names an object that is less than + * <code>other</code>; 0 if this abbreviation exactly matches the + * first {@link #length()} digits of <code>other.name()</code>; + * >0 if this abbreviation names an object that is after + * <code>other</code>. + */ + public int prefixCompare(final AnyObjectId other) { + int cmp; + + cmp = NB.compareUInt32(w1, mask(1, other.w1)); + if (cmp != 0) + return cmp; + + cmp = NB.compareUInt32(w2, mask(2, other.w2)); + if (cmp != 0) + return cmp; + + cmp = NB.compareUInt32(w3, mask(3, other.w3)); + if (cmp != 0) + return cmp; + + cmp = NB.compareUInt32(w4, mask(4, other.w4)); + if (cmp != 0) + return cmp; + + return NB.compareUInt32(w5, mask(5, other.w5)); + } + + private int mask(final int word, final int v) { + return mask(nibbles, word, v); + } + + @Override + public int hashCode() { + return w2; + } + + @Override + public boolean equals(final Object o) { + if (o instanceof AbbreviatedObjectId) { + final AbbreviatedObjectId b = (AbbreviatedObjectId) o; + return nibbles == b.nibbles && w1 == b.w1 && w2 == b.w2 + && w3 == b.w3 && w4 == b.w4 && w5 == b.w5; + } + return false; + } + + /** + * @return string form of the abbreviation, in lower case hexadecimal. + */ + public final String name() { + final char[] b = new char[AnyObjectId.STR_LEN]; + + AnyObjectId.formatHexChar(b, 0, w1); + if (nibbles <= 8) + return new String(b, 0, nibbles); + + AnyObjectId.formatHexChar(b, 8, w2); + if (nibbles <= 16) + return new String(b, 0, nibbles); + + AnyObjectId.formatHexChar(b, 16, w3); + if (nibbles <= 24) + return new String(b, 0, nibbles); + + AnyObjectId.formatHexChar(b, 24, w4); + if (nibbles <= 32) + return new String(b, 0, nibbles); + + AnyObjectId.formatHexChar(b, 32, w5); + return new String(b, 0, nibbles); + } + + @Override + public String toString() { + return "AbbreviatedObjectId[" + name() + "]"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbstractIndexTreeVisitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbstractIndexTreeVisitor.java new file mode 100644 index 0000000000..6052aa336d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbstractIndexTreeVisitor.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.IOException; + +import org.eclipse.jgit.lib.GitIndex.Entry; + +/** + * Implementation of IndexTreeVisitor that can be subclassed if you don't + * case about certain events + * @author dwatson + * + */ +public class AbstractIndexTreeVisitor implements IndexTreeVisitor { + public void finishVisitTree(Tree tree, Tree auxTree, String curDir) + throws IOException { + // Empty + } + + public void finishVisitTree(Tree tree, int i, String curDir) + throws IOException { + // Empty + } + + public void visitEntry(TreeEntry treeEntry, Entry indexEntry, File file) + throws IOException { + // Empty + } + + public void visitEntry(TreeEntry treeEntry, TreeEntry auxEntry, + Entry indexEntry, File file) throws IOException { + // Empty + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AlternateRepositoryDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AlternateRepositoryDatabase.java new file mode 100644 index 0000000000..311839e430 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AlternateRepositoryDatabase.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.util.Collection; + +/** + * An ObjectDatabase of another {@link Repository}. + * <p> + * This {@code ObjectDatabase} wraps around another {@code Repository}'s object + * database, providing its contents to the caller, and closing the Repository + * when this database is closed. The primary user of this class is + * {@link ObjectDirectory}, when the {@code info/alternates} file points at the + * {@code objects/} directory of another repository. + */ +public final class AlternateRepositoryDatabase extends ObjectDatabase { + private final Repository repository; + + private final ObjectDatabase odb; + + /** + * @param alt + * the alternate repository to wrap and export. + */ + public AlternateRepositoryDatabase(final Repository alt) { + repository = alt; + odb = repository.getObjectDatabase(); + } + + /** @return the alternate repository objects are borrowed from. */ + public Repository getRepository() { + return repository; + } + + public void closeSelf() { + repository.close(); + } + + public void create() throws IOException { + repository.create(); + } + + public boolean exists() { + return odb.exists(); + } + + @Override + protected boolean hasObject1(final AnyObjectId objectId) { + return odb.hasObject1(objectId); + } + + @Override + protected boolean tryAgain1() { + return odb.tryAgain1(); + } + + @Override + protected boolean hasObject2(final String objectName) { + return odb.hasObject2(objectName); + } + + @Override + protected ObjectLoader openObject1(final WindowCursor curs, + final AnyObjectId objectId) throws IOException { + return odb.openObject1(curs, objectId); + } + + @Override + protected ObjectLoader openObject2(final WindowCursor curs, + final String objectName, final AnyObjectId objectId) + throws IOException { + return odb.openObject2(curs, objectName, objectId); + } + + @Override + void openObjectInAllPacks1(final Collection<PackedObjectLoader> out, + final WindowCursor curs, final AnyObjectId objectId) + throws IOException { + odb.openObjectInAllPacks1(out, curs, objectId); + } + + @Override + protected ObjectDatabase[] loadAlternates() throws IOException { + return odb.getAlternates(); + } + + @Override + protected void closeAlternates(final ObjectDatabase[] alt) { + // Do nothing; these belong to odb to close, not us. + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java new file mode 100644 index 0000000000..4ed440354d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java @@ -0,0 +1,492 @@ +/* + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.ByteBuffer; + +import org.eclipse.jgit.util.NB; + +/** + * A (possibly mutable) SHA-1 abstraction. + * <p> + * If this is an instance of {@link MutableObjectId} the concept of equality + * with this instance can alter at any time, if this instance is modified to + * represent a different object name. + */ +public abstract class AnyObjectId implements Comparable { + static final int RAW_LEN = Constants.OBJECT_ID_LENGTH; + + static final int STR_LEN = RAW_LEN * 2; + + static { + if (RAW_LEN != 20) + throw new LinkageError("ObjectId expects" + + " Constants.OBJECT_ID_LENGTH = 20; it is " + RAW_LEN + + "."); + } + + /** + * Compare to object identifier byte sequences for equality. + * + * @param firstObjectId + * the first identifier to compare. Must not be null. + * @param secondObjectId + * the second identifier to compare. Must not be null. + * @return true if the two identifiers are the same. + */ + public static boolean equals(final AnyObjectId firstObjectId, + final AnyObjectId secondObjectId) { + if (firstObjectId == secondObjectId) + return true; + + // We test word 2 first as odds are someone already used our + // word 1 as a hash code, and applying that came up with these + // two instances we are comparing for equality. Therefore the + // first two words are very likely to be identical. We want to + // break away from collisions as quickly as possible. + // + return firstObjectId.w2 == secondObjectId.w2 + && firstObjectId.w3 == secondObjectId.w3 + && firstObjectId.w4 == secondObjectId.w4 + && firstObjectId.w5 == secondObjectId.w5 + && firstObjectId.w1 == secondObjectId.w1; + } + + int w1; + + int w2; + + int w3; + + int w4; + + int w5; + + /** + * For ObjectIdMap + * + * @return a discriminator usable for a fan-out style map + */ + public final int getFirstByte() { + return w1 >>> 24; + } + + /** + * Compare this ObjectId to another and obtain a sort ordering. + * + * @param other + * the other id to compare to. Must not be null. + * @return < 0 if this id comes before other; 0 if this id is equal to + * other; > 0 if this id comes after other. + */ + public int compareTo(final ObjectId other) { + if (this == other) + return 0; + + int cmp; + + cmp = NB.compareUInt32(w1, other.w1); + if (cmp != 0) + return cmp; + + cmp = NB.compareUInt32(w2, other.w2); + if (cmp != 0) + return cmp; + + cmp = NB.compareUInt32(w3, other.w3); + if (cmp != 0) + return cmp; + + cmp = NB.compareUInt32(w4, other.w4); + if (cmp != 0) + return cmp; + + return NB.compareUInt32(w5, other.w5); + } + + public int compareTo(final Object other) { + return compareTo(((ObjectId) other)); + } + + int compareTo(final byte[] bs, final int p) { + int cmp; + + cmp = NB.compareUInt32(w1, NB.decodeInt32(bs, p)); + if (cmp != 0) + return cmp; + + cmp = NB.compareUInt32(w2, NB.decodeInt32(bs, p + 4)); + if (cmp != 0) + return cmp; + + cmp = NB.compareUInt32(w3, NB.decodeInt32(bs, p + 8)); + if (cmp != 0) + return cmp; + + cmp = NB.compareUInt32(w4, NB.decodeInt32(bs, p + 12)); + if (cmp != 0) + return cmp; + + return NB.compareUInt32(w5, NB.decodeInt32(bs, p + 16)); + } + + int compareTo(final int[] bs, final int p) { + int cmp; + + cmp = NB.compareUInt32(w1, bs[p]); + if (cmp != 0) + return cmp; + + cmp = NB.compareUInt32(w2, bs[p + 1]); + if (cmp != 0) + return cmp; + + cmp = NB.compareUInt32(w3, bs[p + 2]); + if (cmp != 0) + return cmp; + + cmp = NB.compareUInt32(w4, bs[p + 3]); + if (cmp != 0) + return cmp; + + return NB.compareUInt32(w5, bs[p + 4]); + } + + /** + * Tests if this ObjectId starts with the given abbreviation. + * + * @param abbr + * the abbreviation. + * @return true if this ObjectId begins with the abbreviation; else false. + */ + public boolean startsWith(final AbbreviatedObjectId abbr) { + return abbr.prefixCompare(this) == 0; + } + + public int hashCode() { + return w2; + } + + /** + * Determine if this ObjectId has exactly the same value as another. + * + * @param other + * the other id to compare to. May be null. + * @return true only if both ObjectIds have identical bits. + */ + public boolean equals(final AnyObjectId other) { + return other != null ? equals(this, other) : false; + } + + public boolean equals(final Object o) { + if (o instanceof AnyObjectId) + return equals((AnyObjectId) o); + else + return false; + } + + /** + * Copy this ObjectId to an output writer in raw binary. + * + * @param w + * the buffer to copy to. Must be in big endian order. + */ + public void copyRawTo(final ByteBuffer w) { + w.putInt(w1); + w.putInt(w2); + w.putInt(w3); + w.putInt(w4); + w.putInt(w5); + } + + /** + * Copy this ObjectId to a byte array. + * + * @param b + * the buffer to copy to. + * @param o + * the offset within b to write at. + */ + public void copyRawTo(final byte[] b, final int o) { + NB.encodeInt32(b, o, w1); + NB.encodeInt32(b, o + 4, w2); + NB.encodeInt32(b, o + 8, w3); + NB.encodeInt32(b, o + 12, w4); + NB.encodeInt32(b, o + 16, w5); + } + + /** + * Copy this ObjectId to an int array. + * + * @param b + * the buffer to copy to. + * @param o + * the offset within b to write at. + */ + public void copyRawTo(final int[] b, final int o) { + b[o] = w1; + b[o + 1] = w2; + b[o + 2] = w3; + b[o + 3] = w4; + b[o + 4] = w5; + } + + /** + * Copy this ObjectId to an output writer in raw binary. + * + * @param w + * the stream to write to. + * @throws IOException + * the stream writing failed. + */ + public void copyRawTo(final OutputStream w) throws IOException { + writeRawInt(w, w1); + writeRawInt(w, w2); + writeRawInt(w, w3); + writeRawInt(w, w4); + writeRawInt(w, w5); + } + + private static void writeRawInt(final OutputStream w, int v) + throws IOException { + w.write(v >>> 24); + w.write(v >>> 16); + w.write(v >>> 8); + w.write(v); + } + + /** + * Copy this ObjectId to an output writer in hex format. + * + * @param w + * the stream to copy to. + * @throws IOException + * the stream writing failed. + */ + public void copyTo(final OutputStream w) throws IOException { + w.write(toHexByteArray()); + } + + private byte[] toHexByteArray() { + final byte[] dst = new byte[STR_LEN]; + formatHexByte(dst, 0, w1); + formatHexByte(dst, 8, w2); + formatHexByte(dst, 16, w3); + formatHexByte(dst, 24, w4); + formatHexByte(dst, 32, w5); + return dst; + } + + private static final byte[] hexbyte = { '0', '1', '2', '3', '4', '5', '6', + '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + private static void formatHexByte(final byte[] dst, final int p, int w) { + int o = p + 7; + while (o >= p && w != 0) { + dst[o--] = hexbyte[w & 0xf]; + w >>>= 4; + } + while (o >= p) + dst[o--] = '0'; + } + + /** + * Copy this ObjectId to an output writer in hex format. + * + * @param w + * the stream to copy to. + * @throws IOException + * the stream writing failed. + */ + public void copyTo(final Writer w) throws IOException { + w.write(toHexCharArray()); + } + + /** + * Copy this ObjectId to an output writer in hex format. + * + * @param tmp + * temporary char array to buffer construct into before writing. + * Must be at least large enough to hold 2 digits for each byte + * of object id (40 characters or larger). + * @param w + * the stream to copy to. + * @throws IOException + * the stream writing failed. + */ + public void copyTo(final char[] tmp, final Writer w) throws IOException { + toHexCharArray(tmp); + w.write(tmp, 0, STR_LEN); + } + + /** + * Copy this ObjectId to a StringBuilder in hex format. + * + * @param tmp + * temporary char array to buffer construct into before writing. + * Must be at least large enough to hold 2 digits for each byte + * of object id (40 characters or larger). + * @param w + * the string to append onto. + */ + public void copyTo(final char[] tmp, final StringBuilder w) { + toHexCharArray(tmp); + w.append(tmp, 0, STR_LEN); + } + + private char[] toHexCharArray() { + final char[] dst = new char[STR_LEN]; + toHexCharArray(dst); + return dst; + } + + private void toHexCharArray(final char[] dst) { + formatHexChar(dst, 0, w1); + formatHexChar(dst, 8, w2); + formatHexChar(dst, 16, w3); + formatHexChar(dst, 24, w4); + formatHexChar(dst, 32, w5); + } + + private static final char[] hexchar = { '0', '1', '2', '3', '4', '5', '6', + '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + static void formatHexChar(final char[] dst, final int p, int w) { + int o = p + 7; + while (o >= p && w != 0) { + dst[o--] = hexchar[w & 0xf]; + w >>>= 4; + } + while (o >= p) + dst[o--] = '0'; + } + + @Override + public String toString() { + return "AnyObjectId[" + name() + "]"; + } + + /** + * @return string form of the SHA-1, in lower case hexadecimal. + */ + public final String name() { + return new String(toHexCharArray()); + } + + /** + * @return string form of the SHA-1, in lower case hexadecimal. + */ + public final String getName() { + return name(); + } + + /** + * Return unique abbreviation (prefix) of this object SHA-1. + * <p> + * This method is a utility for <code>abbreviate(repo, 8)</code>. + * + * @param repo + * repository for checking uniqueness within. + * @return SHA-1 abbreviation. + */ + public AbbreviatedObjectId abbreviate(final Repository repo) { + return abbreviate(repo, 8); + } + + /** + * Return unique abbreviation (prefix) of this object SHA-1. + * <p> + * Current implementation is not guaranteeing uniqueness, it just returns + * fixed-length prefix of SHA-1 string. + * + * @param repo + * repository for checking uniqueness within. + * @param len + * minimum length of the abbreviated string. + * @return SHA-1 abbreviation. + */ + public AbbreviatedObjectId abbreviate(final Repository repo, final int len) { + // TODO implement checking for uniqueness + final int a = AbbreviatedObjectId.mask(len, 1, w1); + final int b = AbbreviatedObjectId.mask(len, 2, w2); + final int c = AbbreviatedObjectId.mask(len, 3, w3); + final int d = AbbreviatedObjectId.mask(len, 4, w4); + final int e = AbbreviatedObjectId.mask(len, 5, w5); + return new AbbreviatedObjectId(len, a, b, c, d, e); + } + + /** + * Obtain an immutable copy of this current object name value. + * <p> + * Only returns <code>this</code> if this instance is an unsubclassed + * instance of {@link ObjectId}; otherwise a new instance is returned + * holding the same value. + * <p> + * This method is useful to shed any additional memory that may be tied to + * the subclass, yet retain the unique identity of the object id for future + * lookups within maps and repositories. + * + * @return an immutable copy, using the smallest memory footprint possible. + */ + public final ObjectId copy() { + if (getClass() == ObjectId.class) + return (ObjectId) this; + return new ObjectId(this); + } + + /** + * Obtain an immutable copy of this current object name value. + * <p> + * See {@link #copy()} if <code>this</code> is a possibly subclassed (but + * immutable) identity and the application needs a lightweight identity + * <i>only</i> reference. + * + * @return an immutable copy. May be <code>this</code> if this is already + * an immutable instance. + */ + public abstract ObjectId toObjectId(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BinaryDelta.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BinaryDelta.java new file mode 100644 index 0000000000..461e6d4026 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BinaryDelta.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +/** + * Recreate a stream from a base stream and a GIT pack delta. + * <p> + * This entire class is heavily cribbed from <code>patch-delta.c</code> in the + * GIT project. The original delta patching code was written by Nicolas Pitre + * (<nico@cam.org>). + * </p> + */ +public class BinaryDelta { + + /** + * Apply the changes defined by delta to the data in base, yielding a new + * array of bytes. + * + * @param base + * some byte representing an object of some kind. + * @param delta + * a git pack delta defining the transform from one version to + * another. + * @return patched base + */ + public static final byte[] apply(final byte[] base, final byte[] delta) { + int deltaPtr = 0; + + // Length of the base object (a variable length int). + // + int baseLen = 0; + int c, shift = 0; + do { + c = delta[deltaPtr++] & 0xff; + baseLen |= (c & 0x7f) << shift; + shift += 7; + } while ((c & 0x80) != 0); + if (base.length != baseLen) + throw new IllegalArgumentException("base length incorrect"); + + // Length of the resulting object (a variable length int). + // + int resLen = 0; + shift = 0; + do { + c = delta[deltaPtr++] & 0xff; + resLen |= (c & 0x7f) << shift; + shift += 7; + } while ((c & 0x80) != 0); + + final byte[] result = new byte[resLen]; + int resultPtr = 0; + while (deltaPtr < delta.length) { + final int cmd = delta[deltaPtr++] & 0xff; + if ((cmd & 0x80) != 0) { + // Determine the segment of the base which should + // be copied into the output. The segment is given + // as an offset and a length. + // + int copyOffset = 0; + if ((cmd & 0x01) != 0) + copyOffset = delta[deltaPtr++] & 0xff; + if ((cmd & 0x02) != 0) + copyOffset |= (delta[deltaPtr++] & 0xff) << 8; + if ((cmd & 0x04) != 0) + copyOffset |= (delta[deltaPtr++] & 0xff) << 16; + if ((cmd & 0x08) != 0) + copyOffset |= (delta[deltaPtr++] & 0xff) << 24; + + int copySize = 0; + if ((cmd & 0x10) != 0) + copySize = delta[deltaPtr++] & 0xff; + if ((cmd & 0x20) != 0) + copySize |= (delta[deltaPtr++] & 0xff) << 8; + if ((cmd & 0x40) != 0) + copySize |= (delta[deltaPtr++] & 0xff) << 16; + if (copySize == 0) + copySize = 0x10000; + + System.arraycopy(base, copyOffset, result, resultPtr, copySize); + resultPtr += copySize; + } else if (cmd != 0) { + // Anything else the data is literal within the delta + // itself. + // + System.arraycopy(delta, deltaPtr, result, resultPtr, cmd); + deltaPtr += cmd; + resultPtr += cmd; + } else { + // cmd == 0 has been reserved for future encoding but + // for now its not acceptable. + // + throw new IllegalArgumentException("unsupported command 0"); + } + } + + return result; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java new file mode 100644 index 0000000000..0a4222fc38 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2009, JetBrains s.r.o. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * The configuration file based on the blobs stored in the repository + */ +public class BlobBasedConfig extends Config { + /** + * The constructor from a byte array + * + * @param base + * the base configuration file + * @param blob + * the byte array, should be UTF-8 encoded text. + * @throws ConfigInvalidException + * the byte array is not a valid configuration format. + */ + public BlobBasedConfig(Config base, final byte[] blob) + throws ConfigInvalidException { + super(base); + fromText(RawParseUtils.decode(blob)); + } + + /** + * The constructor from object identifier + * + * @param base + * the base configuration file + * @param r + * the repository + * @param objectId + * the object identifier + * @throws IOException + * the blob cannot be read from the repository. + * @throws ConfigInvalidException + * the blob is not a valid configuration format. + */ + public BlobBasedConfig(Config base, final Repository r, + final ObjectId objectId) throws IOException, ConfigInvalidException { + super(base); + final ObjectLoader loader = r.openBlob(objectId); + if (loader == null) + throw new IOException("Blob not found: " + objectId); + fromText(RawParseUtils.decode(loader.getBytes())); + } + + /** + * The constructor from commit and path + * + * @param base + * the base configuration file + * @param commit + * the commit that contains the object + * @param path + * the path within the tree of the commit + * @throws FileNotFoundException + * the path does not exist in the commit's tree. + * @throws IOException + * the tree and/or blob cannot be accessed. + * @throws ConfigInvalidException + * the blob is not a valid configuration format. + */ + public BlobBasedConfig(Config base, final Commit commit, final String path) + throws FileNotFoundException, IOException, ConfigInvalidException { + super(base); + final ObjectId treeId = commit.getTreeId(); + final Repository r = commit.getRepository(); + final TreeWalk tree = TreeWalk.forPath(r, path, treeId); + if (tree == null) + throw new FileNotFoundException("Entry not found by path: " + path); + final ObjectId blobId = tree.getObjectId(0); + final ObjectLoader loader = tree.getRepository().openBlob(blobId); + if (loader == null) + throw new IOException("Blob not found: " + blobId + " for path: " + + path); + fromText(RawParseUtils.decode(loader.getBytes())); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ByteArrayWindow.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ByteArrayWindow.java new file mode 100644 index 0000000000..8042610310 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ByteArrayWindow.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * A {@link ByteWindow} with an underlying byte array for storage. + */ +final class ByteArrayWindow extends ByteWindow { + private final byte[] array; + + ByteArrayWindow(final PackFile pack, final long o, final byte[] b) { + super(pack, o, b.length); + array = b; + } + + @Override + protected int copy(final int p, final byte[] b, final int o, int n) { + n = Math.min(array.length - p, n); + System.arraycopy(array, p, b, o, n); + return n; + } + + @Override + protected int inflate(final int pos, final byte[] b, int o, + final Inflater inf) throws DataFormatException { + while (!inf.finished()) { + if (inf.needsInput()) { + inf.setInput(array, pos, array.length - pos); + break; + } + o += inf.inflate(b, o, b.length - o); + } + while (!inf.finished() && !inf.needsInput()) + o += inf.inflate(b, o, b.length - o); + return o; + } + + @Override + protected void inflateVerify(final int pos, final Inflater inf) + throws DataFormatException { + while (!inf.finished()) { + if (inf.needsInput()) { + inf.setInput(array, pos, array.length - pos); + break; + } + inf.inflate(verifyGarbageBuffer, 0, verifyGarbageBuffer.length); + } + while (!inf.finished() && !inf.needsInput()) + inf.inflate(verifyGarbageBuffer, 0, verifyGarbageBuffer.length); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ByteBufferWindow.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ByteBufferWindow.java new file mode 100644 index 0000000000..1b29934d28 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ByteBufferWindow.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.nio.ByteBuffer; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * A window for accessing git packs using a {@link ByteBuffer} for storage. + * + * @see ByteWindow + */ +final class ByteBufferWindow extends ByteWindow { + private final ByteBuffer buffer; + + ByteBufferWindow(final PackFile pack, final long o, final ByteBuffer b) { + super(pack, o, b.capacity()); + buffer = b; + } + + @Override + protected int copy(final int p, final byte[] b, final int o, int n) { + final ByteBuffer s = buffer.slice(); + s.position(p); + n = Math.min(s.remaining(), n); + s.get(b, o, n); + return n; + } + + @Override + protected int inflate(final int pos, final byte[] b, int o, + final Inflater inf) throws DataFormatException { + final byte[] tmp = new byte[512]; + final ByteBuffer s = buffer.slice(); + s.position(pos); + while (s.remaining() > 0 && !inf.finished()) { + if (inf.needsInput()) { + final int n = Math.min(s.remaining(), tmp.length); + s.get(tmp, 0, n); + inf.setInput(tmp, 0, n); + } + o += inf.inflate(b, o, b.length - o); + } + while (!inf.finished() && !inf.needsInput()) + o += inf.inflate(b, o, b.length - o); + return o; + } + + @Override + protected void inflateVerify(final int pos, final Inflater inf) + throws DataFormatException { + final byte[] tmp = new byte[512]; + final ByteBuffer s = buffer.slice(); + s.position(pos); + while (s.remaining() > 0 && !inf.finished()) { + if (inf.needsInput()) { + final int n = Math.min(s.remaining(), tmp.length); + s.get(tmp, 0, n); + inf.setInput(tmp, 0, n); + } + inf.inflate(verifyGarbageBuffer, 0, verifyGarbageBuffer.length); + } + while (!inf.finished() && !inf.needsInput()) + inf.inflate(verifyGarbageBuffer, 0, verifyGarbageBuffer.length); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ByteWindow.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ByteWindow.java new file mode 100644 index 0000000000..89bbe7548a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ByteWindow.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * A window of data currently stored within a cache. + * <p> + * All bytes in the window can be assumed to be "immediately available", that is + * they are very likely already in memory, unless the operating system's memory + * is very low and has paged part of this process out to disk. Therefore copying + * bytes from a window is very inexpensive. + * </p> + */ +abstract class ByteWindow { + protected final PackFile pack; + + protected final long start; + + protected final long end; + + protected ByteWindow(final PackFile p, final long s, final int n) { + pack = p; + start = s; + end = start + n; + } + + final int size() { + return (int) (end - start); + } + + final boolean contains(final PackFile neededFile, final long neededPos) { + return pack == neededFile && start <= neededPos && neededPos < end; + } + + /** + * Copy bytes from the window to a caller supplied buffer. + * + * @param pos + * offset within the file to start copying from. + * @param dstbuf + * destination buffer to copy into. + * @param dstoff + * offset within <code>dstbuf</code> to start copying into. + * @param cnt + * number of bytes to copy. This value may exceed the number of + * bytes remaining in the window starting at offset + * <code>pos</code>. + * @return number of bytes actually copied; this may be less than + * <code>cnt</code> if <code>cnt</code> exceeded the number of + * bytes available. + */ + final int copy(long pos, byte[] dstbuf, int dstoff, int cnt) { + return copy((int) (pos - start), dstbuf, dstoff, cnt); + } + + /** + * Copy bytes from the window to a caller supplied buffer. + * + * @param pos + * offset within the window to start copying from. + * @param dstbuf + * destination buffer to copy into. + * @param dstoff + * offset within <code>dstbuf</code> to start copying into. + * @param cnt + * number of bytes to copy. This value may exceed the number of + * bytes remaining in the window starting at offset + * <code>pos</code>. + * @return number of bytes actually copied; this may be less than + * <code>cnt</code> if <code>cnt</code> exceeded the number of + * bytes available. + */ + protected abstract int copy(int pos, byte[] dstbuf, int dstoff, int cnt); + + /** + * Pump bytes into the supplied inflater as input. + * + * @param pos + * offset within the file to start supplying input from. + * @param dstbuf + * destination buffer the inflater should output decompressed + * data to. + * @param dstoff + * current offset within <code>dstbuf</code> to inflate into. + * @param inf + * the inflater to feed input to. The caller is responsible for + * initializing the inflater as multiple windows may need to + * supply data to the same inflater to completely decompress + * something. + * @return updated <code>dstoff</code> based on the number of bytes + * successfully copied into <code>dstbuf</code> by + * <code>inf</code>. If the inflater is not yet finished then + * another window's data must still be supplied as input to finish + * decompression. + * @throws DataFormatException + * the inflater encountered an invalid chunk of data. Data + * stream corruption is likely. + */ + final int inflate(long pos, byte[] dstbuf, int dstoff, Inflater inf) + throws DataFormatException { + return inflate((int) (pos - start), dstbuf, dstoff, inf); + } + + /** + * Pump bytes into the supplied inflater as input. + * + * @param pos + * offset within the window to start supplying input from. + * @param dstbuf + * destination buffer the inflater should output decompressed + * data to. + * @param dstoff + * current offset within <code>dstbuf</code> to inflate into. + * @param inf + * the inflater to feed input to. The caller is responsible for + * initializing the inflater as multiple windows may need to + * supply data to the same inflater to completely decompress + * something. + * @return updated <code>dstoff</code> based on the number of bytes + * successfully copied into <code>dstbuf</code> by + * <code>inf</code>. If the inflater is not yet finished then + * another window's data must still be supplied as input to finish + * decompression. + * @throws DataFormatException + * the inflater encountered an invalid chunk of data. Data + * stream corruption is likely. + */ + protected abstract int inflate(int pos, byte[] dstbuf, int dstoff, + Inflater inf) throws DataFormatException; + + protected static final byte[] verifyGarbageBuffer = new byte[2048]; + + final void inflateVerify(final long pos, final Inflater inf) + throws DataFormatException { + inflateVerify((int) (pos - start), inf); + } + + protected abstract void inflateVerify(int pos, Inflater inf) + throws DataFormatException; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Commit.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Commit.java new file mode 100644 index 0000000000..cdfab7cc38 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Commit.java @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2006-2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.nio.charset.Charset; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.MissingObjectException; + +/** + * Instances of this class represent a Commit object. It represents a snapshot + * in a Git repository, who created it and when. + */ +public class Commit implements Treeish { + private static final ObjectId[] EMPTY_OBJECTID_LIST = new ObjectId[0]; + + private final Repository objdb; + + private ObjectId commitId; + + private ObjectId treeId; + + private ObjectId[] parentIds; + + private PersonIdent author; + + private PersonIdent committer; + + private String message; + + private Tree treeObj; + + private byte[] raw; + + private Charset encoding; + + /** + * Create an empty commit object. More information must be fed to this + * object to make it useful. + * + * @param db + * The repository with which to associate it. + */ + public Commit(final Repository db) { + objdb = db; + parentIds = EMPTY_OBJECTID_LIST; + } + + /** + * Create a commit associated with these parents and associate it with a + * repository. + * + * @param db + * The repository to which this commit object belongs + * @param parentIds + * Id's of the parent(s) + */ + public Commit(final Repository db, final ObjectId[] parentIds) { + objdb = db; + this.parentIds = parentIds; + } + + /** + * Create a commit object with the specified id and data from and existing + * commit object in a repository. + * + * @param db + * The repository to which this commit object belongs + * @param id + * Commit id + * @param raw + * Raw commit object data + */ + public Commit(final Repository db, final ObjectId id, final byte[] raw) { + objdb = db; + commitId = id; + treeId = ObjectId.fromString(raw, 5); + parentIds = new ObjectId[1]; + int np=0; + int rawPtr = 46; + for (;;) { + if (raw[rawPtr] != 'p') + break; + if (np == 0) { + parentIds[np++] = ObjectId.fromString(raw, rawPtr + 7); + } else if (np == 1) { + parentIds = new ObjectId[] { parentIds[0], ObjectId.fromString(raw, rawPtr + 7) }; + np++; + } else { + if (parentIds.length <= np) { + ObjectId[] old = parentIds; + parentIds = new ObjectId[parentIds.length+32]; + for (int i=0; i<np; ++i) + parentIds[i] = old[i]; + } + parentIds[np++] = ObjectId.fromString(raw, rawPtr + 7); + } + rawPtr += 48; + } + if (np != parentIds.length) { + ObjectId[] old = parentIds; + parentIds = new ObjectId[np]; + for (int i=0; i<np; ++i) + parentIds[i] = old[i]; + } else + if (np == 0) + parentIds = EMPTY_OBJECTID_LIST; + this.raw = raw; + } + + /** + * @return get repository for the commit + */ + public Repository getRepository() { + return objdb; + } + + /** + * @return The commit object id + */ + public ObjectId getCommitId() { + return commitId; + } + + /** + * Set the id of this object. + * + * @param id + * the id that we calculated for this object. + */ + public void setCommitId(final ObjectId id) { + commitId = id; + } + + public ObjectId getTreeId() { + return treeId; + } + + /** + * Set the tree id for this commit object + * + * @param id + */ + public void setTreeId(final ObjectId id) { + if (treeId==null || !treeId.equals(id)) { + treeObj = null; + } + treeId = id; + } + + public Tree getTree() throws IOException { + if (treeObj == null) { + treeObj = objdb.mapTree(getTreeId()); + if (treeObj == null) { + throw new MissingObjectException(getTreeId(), + Constants.TYPE_TREE); + } + } + return treeObj; + } + + /** + * Set the tree object for this commit + * @see #setTreeId + * @param t the Tree object + */ + public void setTree(final Tree t) { + treeId = t.getTreeId(); + treeObj = t; + } + + /** + * @return the author and authoring time for this commit + */ + public PersonIdent getAuthor() { + decode(); + return author; + } + + /** + * Set the author and authoring time for this commit + * @param a + */ + public void setAuthor(final PersonIdent a) { + author = a; + } + + /** + * @return the committer and commit time for this object + */ + public PersonIdent getCommitter() { + decode(); + return committer; + } + + /** + * Set the committer and commit time for this object + * @param c the committer information + */ + public void setCommitter(final PersonIdent c) { + committer = c; + } + + /** + * @return the object ids of this commit + */ + public ObjectId[] getParentIds() { + return parentIds; + } + + /** + * @return the commit message + */ + public String getMessage() { + decode(); + return message; + } + + /** + * Set the parents of this commit + * @param parentIds + */ + public void setParentIds(ObjectId[] parentIds) { + this.parentIds = new ObjectId[parentIds.length]; + for (int i=0; i<parentIds.length; ++i) + this.parentIds[i] = parentIds[i]; + } + + private void decode() { + // FIXME: handle I/O errors + if (raw != null) { + try { + DataInputStream br = new DataInputStream(new ByteArrayInputStream(raw)); + String n = br.readLine(); + if (n == null || !n.startsWith("tree ")) { + throw new CorruptObjectException(commitId, "no tree"); + } + while ((n = br.readLine()) != null && n.startsWith("parent ")) { + // empty body + } + if (n == null || !n.startsWith("author ")) { + throw new CorruptObjectException(commitId, "no author"); + } + String rawAuthor = n.substring("author ".length()); + n = br.readLine(); + if (n == null || !n.startsWith("committer ")) { + throw new CorruptObjectException(commitId, "no committer"); + } + String rawCommitter = n.substring("committer ".length()); + n = br.readLine(); + if (n != null && n.startsWith( "encoding")) + encoding = Charset.forName(n.substring("encoding ".length())); + else + if (n == null || !n.equals("")) { + throw new CorruptObjectException(commitId, + "malformed header:"+n); + } + byte[] readBuf = new byte[br.available()]; // in-memory stream so this is all bytes left + br.read(readBuf); + int msgstart = readBuf.length != 0 ? ( readBuf[0] == '\n' ? 1 : 0 ) : 0; + + if (encoding != null) { + // TODO: this isn't reliable so we need to guess the encoding from the actual content + author = new PersonIdent(new String(rawAuthor.getBytes(),encoding.name())); + committer = new PersonIdent(new String(rawCommitter.getBytes(),encoding.name())); + message = new String(readBuf,msgstart, readBuf.length-msgstart, encoding.name()); + } else { + // TODO: use config setting / platform / ascii / iso-latin + author = new PersonIdent(new String(rawAuthor.getBytes())); + committer = new PersonIdent(new String(rawCommitter.getBytes())); + message = new String(readBuf, msgstart, readBuf.length-msgstart); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + raw = null; + } + } + } + + /** + * Set the commit message + * + * @param m the commit message + */ + public void setMessage(final String m) { + message = m; + } + + /** + * Persist this commit object + * + * @throws IOException + */ + public void commit() throws IOException { + if (getCommitId() != null) + throw new IllegalStateException("exists " + getCommitId()); + setCommitId(new ObjectWriter(objdb).writeCommit(this)); + } + + public String toString() { + return "Commit[" + ObjectId.toString(getCommitId()) + " " + getAuthor() + "]"; + } + + /** + * State the encoding for the commit information + * + * @param e + * the encoding. See {@link Charset} + */ + public void setEncoding(String e) { + encoding = Charset.forName(e); + } + + /** + * State the encoding for the commit information + * + * @param e + * the encoding. See {@link Charset} + */ + public void setEncoding(Charset e) { + encoding = e; + } + + /** + * @return the encoding used. See {@link Charset} + */ + public String getEncoding() { + if (encoding != null) + return encoding.name(); + else + return null; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java new file mode 100644 index 0000000000..b6b9c5e05b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java @@ -0,0 +1,1154 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2009, Google, Inc. + * Copyright (C) 2009, JetBrains s.r.o. + * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * Copyright (C) 2008, Thad Hughes <thadh@thad.corp.google.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.util.StringUtils; + + +/** + * Git style {@code .config}, {@code .gitconfig}, {@code .gitmodules} file. + */ +public class Config { + private static final String[] EMPTY_STRING_ARRAY = {}; + private static final long KiB = 1024; + private static final long MiB = 1024 * KiB; + private static final long GiB = 1024 * MiB; + + /** + * Immutable current state of the configuration data. + * <p> + * This state is copy-on-write. It should always contain an immutable list + * of the configuration keys/values. + */ + private final AtomicReference<State> state; + + private final Config baseConfig; + + /** + * Magic value indicating a missing entry. + * <p> + * This value is tested for reference equality in some contexts, so we + * must ensure it is a special copy of the empty string. It also must + * be treated like the empty string. + */ + private static final String MAGIC_EMPTY_VALUE = new String(); + + /** Create a configuration with no default fallback. */ + public Config() { + this(null); + } + + /** + * Create an empty configuration with a fallback for missing keys. + * + * @param defaultConfig + * the base configuration to be consulted when a key is missing + * from this configuration instance. + */ + public Config(Config defaultConfig) { + baseConfig = defaultConfig; + state = new AtomicReference<State>(newState()); + } + + /** + * Escape the value before saving + * + * @param x + * the value to escape + * @return the escaped value + */ + private static String escapeValue(final String x) { + boolean inquote = false; + int lineStart = 0; + final StringBuffer r = new StringBuffer(x.length()); + for (int k = 0; k < x.length(); k++) { + final char c = x.charAt(k); + switch (c) { + case '\n': + if (inquote) { + r.append('"'); + inquote = false; + } + r.append("\\n\\\n"); + lineStart = r.length(); + break; + + case '\t': + r.append("\\t"); + break; + + case '\b': + r.append("\\b"); + break; + + case '\\': + r.append("\\\\"); + break; + + case '"': + r.append("\\\""); + break; + + case ';': + case '#': + if (!inquote) { + r.insert(lineStart, '"'); + inquote = true; + } + r.append(c); + break; + + case ' ': + if (!inquote && r.length() > 0 + && r.charAt(r.length() - 1) == ' ') { + r.insert(lineStart, '"'); + inquote = true; + } + r.append(' '); + break; + + default: + r.append(c); + break; + } + } + if (inquote) { + r.append('"'); + } + return r.toString(); + } + + /** + * Obtain an integer value from the configuration. + * + * @param section + * section the key is grouped within. + * @param name + * name of the key to get. + * @param defaultValue + * default value to return if no value was present. + * @return an integer value from the configuration, or defaultValue. + */ + public int getInt(final String section, final String name, + final int defaultValue) { + return getInt(section, null, name, defaultValue); + } + + /** + * Obtain an integer value from the configuration. + * + * @param section + * section the key is grouped within. + * @param subsection + * subsection name, such a remote or branch name. + * @param name + * name of the key to get. + * @param defaultValue + * default value to return if no value was present. + * @return an integer value from the configuration, or defaultValue. + */ + public int getInt(final String section, String subsection, + final String name, final int defaultValue) { + final long val = getLong(section, subsection, name, defaultValue); + if (Integer.MIN_VALUE <= val && val <= Integer.MAX_VALUE) + return (int) val; + throw new IllegalArgumentException("Integer value " + section + "." + + name + " out of range"); + } + + /** + * Obtain an integer value from the configuration. + * + * @param section + * section the key is grouped within. + * @param subsection + * subsection name, such a remote or branch name. + * @param name + * name of the key to get. + * @param defaultValue + * default value to return if no value was present. + * @return an integer value from the configuration, or defaultValue. + */ + public long getLong(final String section, String subsection, + final String name, final long defaultValue) { + final String str = getString(section, subsection, name); + if (str == null) + return defaultValue; + + String n = str.trim(); + if (n.length() == 0) + return defaultValue; + + long mul = 1; + switch (StringUtils.toLowerCase(n.charAt(n.length() - 1))) { + case 'g': + mul = GiB; + break; + case 'm': + mul = MiB; + break; + case 'k': + mul = KiB; + break; + } + if (mul > 1) + n = n.substring(0, n.length() - 1).trim(); + if (n.length() == 0) + return defaultValue; + + try { + return mul * Long.parseLong(n); + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException("Invalid integer value: " + + section + "." + name + "=" + str); + } + } + + /** + * Get a boolean value from the git config + * + * @param section + * section the key is grouped within. + * @param name + * name of the key to get. + * @param defaultValue + * default value to return if no value was present. + * @return true if any value or defaultValue is true, false for missing or + * explicit false + */ + public boolean getBoolean(final String section, final String name, + final boolean defaultValue) { + return getBoolean(section, null, name, defaultValue); + } + + /** + * Get a boolean value from the git config + * + * @param section + * section the key is grouped within. + * @param subsection + * subsection name, such a remote or branch name. + * @param name + * name of the key to get. + * @param defaultValue + * default value to return if no value was present. + * @return true if any value or defaultValue is true, false for missing or + * explicit false + */ + public boolean getBoolean(final String section, String subsection, + final String name, final boolean defaultValue) { + String n = getRawString(section, subsection, name); + if (n == null) + return defaultValue; + + if (MAGIC_EMPTY_VALUE == n || StringUtils.equalsIgnoreCase("yes", n) + || StringUtils.equalsIgnoreCase("true", n) + || StringUtils.equalsIgnoreCase("1", n) + || StringUtils.equalsIgnoreCase("on", n)) { + return true; + + } else if (StringUtils.equalsIgnoreCase("no", n) + || StringUtils.equalsIgnoreCase("false", n) + || StringUtils.equalsIgnoreCase("0", n) + || StringUtils.equalsIgnoreCase("off", n)) { + return false; + + } else { + throw new IllegalArgumentException("Invalid boolean value: " + + section + "." + name + "=" + n); + } + } + + /** + * Get string value + * + * @param section + * the section + * @param subsection + * the subsection for the value + * @param name + * the key name + * @return a String value from git config. + */ + public String getString(final String section, String subsection, + final String name) { + return getRawString(section, subsection, name); + } + + /** + * Get a list of string values + * <p> + * If this instance was created with a base, the base's values are returned + * first (if any). + * + * @param section + * the section + * @param subsection + * the subsection for the value + * @param name + * the key name + * @return array of zero or more values from the configuration. + */ + public String[] getStringList(final String section, String subsection, + final String name) { + final String[] baseList; + if (baseConfig != null) + baseList = baseConfig.getStringList(section, subsection, name); + else + baseList = EMPTY_STRING_ARRAY; + + final List<String> lst = getRawStringList(section, subsection, name); + if (lst != null) { + final String[] res = new String[baseList.length + lst.size()]; + int idx = baseList.length; + System.arraycopy(baseList, 0, res, 0, idx); + for (final String val : lst) + res[idx++] = val; + return res; + } + return baseList; + } + + /** + * @param section + * section to search for. + * @return set of all subsections of specified section within this + * configuration and its base configuration; may be empty if no + * subsection exists. + */ + public Set<String> getSubsections(final String section) { + return get(new SubsectionNames(section)); + } + + /** + * Obtain a handle to a parsed set of configuration values. + * + * @param <T> + * type of configuration model to return. + * @param parser + * parser which can create the model if it is not already + * available in this configuration file. The parser is also used + * as the key into a cache and must obey the hashCode and equals + * contract in order to reuse a parsed model. + * @return the parsed object instance, which is cached inside this config. + */ + @SuppressWarnings("unchecked") + public <T> T get(final SectionParser<T> parser) { + final State myState = getState(); + T obj = (T) myState.cache.get(parser); + if (obj == null) { + obj = parser.parse(this); + myState.cache.put(parser, obj); + } + return obj; + } + + /** + * Remove a cached configuration object. + * <p> + * If the associated configuration object has not yet been cached, this + * method has no effect. + * + * @param parser + * parser used to obtain the configuration object. + * @see #get(SectionParser) + */ + public void uncache(final SectionParser<?> parser) { + state.get().cache.remove(parser); + } + + private String getRawString(final String section, final String subsection, + final String name) { + final List<String> lst = getRawStringList(section, subsection, name); + if (lst != null) + return lst.get(0); + else if (baseConfig != null) + return baseConfig.getRawString(section, subsection, name); + else + return null; + } + + private List<String> getRawStringList(final String section, + final String subsection, final String name) { + List<String> r = null; + for (final Entry e : state.get().entryList) { + if (e.match(section, subsection, name)) + r = add(r, e.value); + } + return r; + } + + private static List<String> add(final List<String> curr, final String value) { + if (curr == null) + return Collections.singletonList(value); + if (curr.size() == 1) { + final List<String> r = new ArrayList<String>(2); + r.add(curr.get(0)); + r.add(value); + return r; + } + curr.add(value); + return curr; + } + + private State getState() { + State cur, upd; + do { + cur = state.get(); + final State base = getBaseState(); + if (cur.baseState == base) + return cur; + upd = new State(cur.entryList, base); + } while (!state.compareAndSet(cur, upd)); + return upd; + } + + private State getBaseState() { + return baseConfig != null ? baseConfig.getState() : null; + } + + /** + * Add or modify a configuration value. The parameters will result in a + * configuration entry like this. + * + * <pre> + * [section "subsection"] + * name = value + * </pre> + * + * @param section + * section name, e.g "branch" + * @param subsection + * optional subsection value, e.g. a branch name + * @param name + * parameter name, e.g. "filemode" + * @param value + * parameter value + */ + public void setInt(final String section, final String subsection, + final String name, final int value) { + setLong(section, subsection, name, value); + } + + /** + * Add or modify a configuration value. The parameters will result in a + * configuration entry like this. + * + * <pre> + * [section "subsection"] + * name = value + * </pre> + * + * @param section + * section name, e.g "branch" + * @param subsection + * optional subsection value, e.g. a branch name + * @param name + * parameter name, e.g. "filemode" + * @param value + * parameter value + */ + public void setLong(final String section, final String subsection, + final String name, final long value) { + final String s; + + if (value >= GiB && (value % GiB) == 0) + s = String.valueOf(value / GiB) + " g"; + else if (value >= MiB && (value % MiB) == 0) + s = String.valueOf(value / MiB) + " m"; + else if (value >= KiB && (value % KiB) == 0) + s = String.valueOf(value / KiB) + " k"; + else + s = String.valueOf(value); + + setString(section, subsection, name, s); + } + + /** + * Add or modify a configuration value. The parameters will result in a + * configuration entry like this. + * + * <pre> + * [section "subsection"] + * name = value + * </pre> + * + * @param section + * section name, e.g "branch" + * @param subsection + * optional subsection value, e.g. a branch name + * @param name + * parameter name, e.g. "filemode" + * @param value + * parameter value + */ + public void setBoolean(final String section, final String subsection, + final String name, final boolean value) { + setString(section, subsection, name, value ? "true" : "false"); + } + + /** + * Add or modify a configuration value. The parameters will result in a + * configuration entry like this. + * + * <pre> + * [section "subsection"] + * name = value + * </pre> + * + * @param section + * section name, e.g "branch" + * @param subsection + * optional subsection value, e.g. a branch name + * @param name + * parameter name, e.g. "filemode" + * @param value + * parameter value, e.g. "true" + */ + public void setString(final String section, final String subsection, + final String name, final String value) { + setStringList(section, subsection, name, Collections + .singletonList(value)); + } + + /** + * Remove a configuration value. + * + * @param section + * section name, e.g "branch" + * @param subsection + * optional subsection value, e.g. a branch name + * @param name + * parameter name, e.g. "filemode" + */ + public void unset(final String section, final String subsection, + final String name) { + setStringList(section, subsection, name, Collections + .<String> emptyList()); + } + + /** + * Set a configuration value. + * + * <pre> + * [section "subsection"] + * name = value + * </pre> + * + * @param section + * section name, e.g "branch" + * @param subsection + * optional subsection value, e.g. a branch name + * @param name + * parameter name, e.g. "filemode" + * @param values + * list of zero or more values for this key. + */ + public void setStringList(final String section, final String subsection, + final String name, final List<String> values) { + State src, res; + do { + src = state.get(); + res = replaceStringList(src, section, subsection, name, values); + } while (!state.compareAndSet(src, res)); + } + + private State replaceStringList(final State srcState, + final String section, final String subsection, final String name, + final List<String> values) { + final List<Entry> entries = copy(srcState, values); + int entryIndex = 0; + int valueIndex = 0; + int insertPosition = -1; + + // Reset the first n Entry objects that match this input name. + // + while (entryIndex < entries.size() && valueIndex < values.size()) { + final Entry e = entries.get(entryIndex); + if (e.match(section, subsection, name)) { + entries.set(entryIndex, e.forValue(values.get(valueIndex++))); + insertPosition = entryIndex + 1; + } + entryIndex++; + } + + // Remove any extra Entry objects that we no longer need. + // + if (valueIndex == values.size() && entryIndex < entries.size()) { + while (entryIndex < entries.size()) { + final Entry e = entries.get(entryIndex++); + if (e.match(section, subsection, name)) + entries.remove(--entryIndex); + } + } + + // Insert new Entry objects for additional/new values. + // + if (valueIndex < values.size() && entryIndex == entries.size()) { + if (insertPosition < 0) { + // We didn't find a matching key above, but maybe there + // is already a section available that matches. Insert + // after the last key of that section. + // + insertPosition = findSectionEnd(entries, section, subsection); + } + if (insertPosition < 0) { + // We didn't find any matching section header for this key, + // so we must create a new section header at the end. + // + final Entry e = new Entry(); + e.section = section; + e.subsection = subsection; + entries.add(e); + insertPosition = entries.size(); + } + while (valueIndex < values.size()) { + final Entry e = new Entry(); + e.section = section; + e.subsection = subsection; + e.name = name; + e.value = values.get(valueIndex++); + entries.add(insertPosition++, e); + } + } + + return newState(entries); + } + + private static List<Entry> copy(final State src, final List<String> values) { + // At worst we need to insert 1 line for each value, plus 1 line + // for a new section header. Assume that and allocate the space. + // + final int max = src.entryList.size() + values.size() + 1; + final ArrayList<Entry> r = new ArrayList<Entry>(max); + r.addAll(src.entryList); + return r; + } + + private static int findSectionEnd(final List<Entry> entries, + final String section, final String subsection) { + for (int i = 0; i < entries.size(); i++) { + Entry e = entries.get(i); + if (e.match(section, subsection, null)) { + i++; + while (i < entries.size()) { + e = entries.get(i); + if (e.match(section, subsection, e.name)) + i++; + else + break; + } + return i; + } + } + return -1; + } + + /** + * @return this configuration, formatted as a Git style text file. + */ + public String toText() { + final StringBuilder out = new StringBuilder(); + for (final Entry e : state.get().entryList) { + if (e.prefix != null) + out.append(e.prefix); + if (e.section != null && e.name == null) { + out.append('['); + out.append(e.section); + if (e.subsection != null) { + out.append(' '); + out.append('"'); + out.append(escapeValue(e.subsection)); + out.append('"'); + } + out.append(']'); + } else if (e.section != null && e.name != null) { + if (e.prefix == null || "".equals(e.prefix)) + out.append('\t'); + out.append(e.name); + if (e.value != null) { + if (MAGIC_EMPTY_VALUE != e.value) { + out.append(" = "); + out.append(escapeValue(e.value)); + } + } + if (e.suffix != null) + out.append(' '); + } + if (e.suffix != null) + out.append(e.suffix); + out.append('\n'); + } + return out.toString(); + } + + /** + * Clear this configuration and reset to the contents of the parsed string. + * + * @param text + * Git style text file listing configuration properties. + * @throws ConfigInvalidException + * the text supplied is not formatted correctly. No changes were + * made to {@code this}. + */ + public void fromText(final String text) throws ConfigInvalidException { + final List<Entry> newEntries = new ArrayList<Entry>(); + final StringReader in = new StringReader(text); + Entry last = null; + Entry e = new Entry(); + for (;;) { + int input = in.read(); + if (-1 == input) + break; + + final char c = (char) input; + if ('\n' == c) { + // End of this entry. + newEntries.add(e); + if (e.section != null) + last = e; + e = new Entry(); + + } else if (e.suffix != null) { + // Everything up until the end-of-line is in the suffix. + e.suffix += c; + + } else if (';' == c || '#' == c) { + // The rest of this line is a comment; put into suffix. + e.suffix = String.valueOf(c); + + } else if (e.section == null && Character.isWhitespace(c)) { + // Save the leading whitespace (if any). + if (e.prefix == null) + e.prefix = ""; + e.prefix += c; + + } else if ('[' == c) { + // This is a section header. + e.section = readSectionName(in); + input = in.read(); + if ('"' == input) { + e.subsection = readValue(in, true, '"'); + input = in.read(); + } + if (']' != input) + throw new ConfigInvalidException("Bad group header"); + e.suffix = ""; + + } else if (last != null) { + // Read a value. + e.section = last.section; + e.subsection = last.subsection; + in.reset(); + e.name = readKeyName(in); + if (e.name.endsWith("\n")) { + e.name = e.name.substring(0, e.name.length() - 1); + e.value = MAGIC_EMPTY_VALUE; + } else + e.value = readValue(in, false, -1); + + } else + throw new ConfigInvalidException("Invalid line in config file"); + } + + state.set(newState(newEntries)); + } + + private State newState() { + return new State(Collections.<Entry> emptyList(), getBaseState()); + } + + private State newState(final List<Entry> entries) { + return new State(Collections.unmodifiableList(entries), getBaseState()); + } + + /** + * Clear the configuration file + */ + protected void clear() { + state.set(newState()); + } + + private static String readSectionName(final StringReader in) + throws ConfigInvalidException { + final StringBuilder name = new StringBuilder(); + for (;;) { + int c = in.read(); + if (c < 0) + throw new ConfigInvalidException("Unexpected end of config file"); + + if (']' == c) { + in.reset(); + break; + } + + if (' ' == c || '\t' == c) { + for (;;) { + c = in.read(); + if (c < 0) + throw new ConfigInvalidException("Unexpected end of config file"); + + if ('"' == c) { + in.reset(); + break; + } + + if (' ' == c || '\t' == c) + continue; // Skipped... + throw new ConfigInvalidException("Bad section entry: " + name); + } + break; + } + + if (Character.isLetterOrDigit((char) c) || '.' == c || '-' == c) + name.append((char) c); + else + throw new ConfigInvalidException("Bad section entry: " + name); + } + return name.toString(); + } + + private static String readKeyName(final StringReader in) + throws ConfigInvalidException { + final StringBuffer name = new StringBuffer(); + for (;;) { + int c = in.read(); + if (c < 0) + throw new ConfigInvalidException("Unexpected end of config file"); + + if ('=' == c) + break; + + if (' ' == c || '\t' == c) { + for (;;) { + c = in.read(); + if (c < 0) + throw new ConfigInvalidException("Unexpected end of config file"); + + if ('=' == c) + break; + + if (';' == c || '#' == c || '\n' == c) { + in.reset(); + break; + } + + if (' ' == c || '\t' == c) + continue; // Skipped... + throw new ConfigInvalidException("Bad entry delimiter"); + } + break; + } + + if (Character.isLetterOrDigit((char) c) || c == '-') { + // From the git-config man page: + // The variable names are case-insensitive and only + // alphanumeric characters and - are allowed. + name.append((char) c); + } else if ('\n' == c) { + in.reset(); + name.append((char) c); + break; + } else + throw new ConfigInvalidException("Bad entry name: " + name); + } + return name.toString(); + } + + private static String readValue(final StringReader in, boolean quote, + final int eol) throws ConfigInvalidException { + final StringBuffer value = new StringBuffer(); + boolean space = false; + for (;;) { + int c = in.read(); + if (c < 0) { + if (value.length() == 0) + throw new ConfigInvalidException("Unexpected end of config file"); + break; + } + + if ('\n' == c) { + if (quote) + throw new ConfigInvalidException("Newline in quotes not allowed"); + in.reset(); + break; + } + + if (eol == c) + break; + + if (!quote) { + if (Character.isWhitespace((char) c)) { + space = true; + continue; + } + if (';' == c || '#' == c) { + in.reset(); + break; + } + } + + if (space) { + if (value.length() > 0) + value.append(' '); + space = false; + } + + if ('\\' == c) { + c = in.read(); + switch (c) { + case -1: + throw new ConfigInvalidException("End of file in escape"); + case '\n': + continue; + case 't': + value.append('\t'); + continue; + case 'b': + value.append('\b'); + continue; + case 'n': + value.append('\n'); + continue; + case '\\': + value.append('\\'); + continue; + case '"': + value.append('"'); + continue; + default: + throw new ConfigInvalidException("Bad escape: " + ((char) c)); + } + } + + if ('"' == c) { + quote = !quote; + continue; + } + + value.append((char) c); + } + return value.length() > 0 ? value.toString() : null; + } + + /** + * Parses a section of the configuration into an application model object. + * <p> + * Instances must implement hashCode and equals such that model objects can + * be cached by using the {@code SectionParser} as a key of a HashMap. + * <p> + * As the {@code SectionParser} itself is used as the key of the internal + * HashMap applications should be careful to ensure the SectionParser key + * does not retain unnecessary application state which may cause memory to + * be held longer than expected. + * + * @param <T> + * type of the application model created by the parser. + */ + public static interface SectionParser<T> { + /** + * Create a model object from a configuration. + * + * @param cfg + * the configuration to read values from. + * @return the application model instance. + */ + T parse(Config cfg); + } + + private static class SubsectionNames implements SectionParser<Set<String>> { + private final String section; + + SubsectionNames(final String sectionName) { + section = sectionName; + } + + public int hashCode() { + return section.hashCode(); + } + + public boolean equals(Object other) { + if (other instanceof SubsectionNames) { + return section.equals(((SubsectionNames) other).section); + } + return false; + } + + public Set<String> parse(Config cfg) { + final Set<String> result = new HashSet<String>(); + while (cfg != null) { + for (final Entry e : cfg.state.get().entryList) { + if (e.subsection != null && e.name == null + && StringUtils.equalsIgnoreCase(section, e.section)) + result.add(e.subsection); + } + cfg = cfg.baseConfig; + } + return Collections.unmodifiableSet(result); + } + } + + private static class State { + final List<Entry> entryList; + + final Map<Object, Object> cache; + + final State baseState; + + State(List<Entry> entries, State base) { + entryList = entries; + cache = new ConcurrentHashMap<Object, Object>(16, 0.75f, 1); + baseState = base; + } + } + + /** + * The configuration file entry + */ + private static class Entry { + /** + * The text content before entry + */ + String prefix; + + /** + * The section name for the entry + */ + String section; + + /** + * Subsection name + */ + String subsection; + + /** + * The key name + */ + String name; + + /** + * The value + */ + String value; + + /** + * The text content after entry + */ + String suffix; + + Entry forValue(final String newValue) { + final Entry e = new Entry(); + e.prefix = prefix; + e.section = section; + e.subsection = subsection; + e.name = name; + e.value = newValue; + e.suffix = suffix; + return e; + } + + boolean match(final String aSection, final String aSubsection, + final String aKey) { + return eqIgnoreCase(section, aSection) + && eqSameCase(subsection, aSubsection) + && eqIgnoreCase(name, aKey); + } + + private static boolean eqIgnoreCase(final String a, final String b) { + if (a == null && b == null) + return true; + if (a == null || b == null) + return false; + return StringUtils.equalsIgnoreCase(a, b); + } + + private static boolean eqSameCase(final String a, final String b) { + if (a == null && b == null) + return true; + if (a == null || b == null) + return false; + return a.equals(b); + } + } + + private static class StringReader { + private final char[] buf; + + private int pos; + + StringReader(final String in) { + buf = in.toCharArray(); + } + + int read() { + try { + return buf[pos++]; + } catch (ArrayIndexOutOfBoundsException e) { + pos = buf.length; + return -1; + } + } + + void reset() { + pos--; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java new file mode 100644 index 0000000000..403d0dbeed --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.util.MutableInteger; + +/** Misc. constants used throughout JGit. */ +public final class Constants { + /** Hash function used natively by Git for all objects. */ + private static final String HASH_FUNCTION = "SHA-1"; + + /** Length of an object hash. */ + public static final int OBJECT_ID_LENGTH = 20; + + /** Special name for the "HEAD" symbolic-ref. */ + public static final String HEAD = "HEAD"; + + /** + * Text string that identifies an object as a commit. + * <p> + * Commits connect trees into a string of project histories, where each + * commit is an assertion that the best way to continue is to use this other + * tree (set of files). + */ + public static final String TYPE_COMMIT = "commit"; + + /** + * Text string that identifies an object as a blob. + * <p> + * Blobs store whole file revisions. They are used for any user file, as + * well as for symlinks. Blobs form the bulk of any project's storage space. + */ + public static final String TYPE_BLOB = "blob"; + + /** + * Text string that identifies an object as a tree. + * <p> + * Trees attach object ids (hashes) to names and file modes. The normal use + * for a tree is to store a version of a directory and its contents. + */ + public static final String TYPE_TREE = "tree"; + + /** + * Text string that identifies an object as an annotated tag. + * <p> + * Annotated tags store a pointer to any other object, and an additional + * message. It is most commonly used to record a stable release of the + * project. + */ + public static final String TYPE_TAG = "tag"; + + private static final byte[] ENCODED_TYPE_COMMIT = encodeASCII(TYPE_COMMIT); + + private static final byte[] ENCODED_TYPE_BLOB = encodeASCII(TYPE_BLOB); + + private static final byte[] ENCODED_TYPE_TREE = encodeASCII(TYPE_TREE); + + private static final byte[] ENCODED_TYPE_TAG = encodeASCII(TYPE_TAG); + + /** An unknown or invalid object type code. */ + public static final int OBJ_BAD = -1; + + /** + * In-pack object type: extended types. + * <p> + * This header code is reserved for future expansion. It is currently + * undefined/unsupported. + */ + public static final int OBJ_EXT = 0; + + /** + * In-pack object type: commit. + * <p> + * Indicates the associated object is a commit. + * <p> + * <b>This constant is fixed and is defined by the Git packfile format.</b> + * + * @see #TYPE_COMMIT + */ + public static final int OBJ_COMMIT = 1; + + /** + * In-pack object type: tree. + * <p> + * Indicates the associated object is a tree. + * <p> + * <b>This constant is fixed and is defined by the Git packfile format.</b> + * + * @see #TYPE_BLOB + */ + public static final int OBJ_TREE = 2; + + /** + * In-pack object type: blob. + * <p> + * Indicates the associated object is a blob. + * <p> + * <b>This constant is fixed and is defined by the Git packfile format.</b> + * + * @see #TYPE_BLOB + */ + public static final int OBJ_BLOB = 3; + + /** + * In-pack object type: annotated tag. + * <p> + * Indicates the associated object is an annotated tag. + * <p> + * <b>This constant is fixed and is defined by the Git packfile format.</b> + * + * @see #TYPE_TAG + */ + public static final int OBJ_TAG = 4; + + /** In-pack object type: reserved for future use. */ + public static final int OBJ_TYPE_5 = 5; + + /** + * In-pack object type: offset delta + * <p> + * Objects stored with this type actually have a different type which must + * be obtained from their delta base object. Delta objects store only the + * changes needed to apply to the base object in order to recover the + * original object. + * <p> + * An offset delta uses a negative offset from the start of this object to + * refer to its delta base. The base object must exist in this packfile + * (even in the case of a thin pack). + * <p> + * <b>This constant is fixed and is defined by the Git packfile format.</b> + */ + public static final int OBJ_OFS_DELTA = 6; + + /** + * In-pack object type: reference delta + * <p> + * Objects stored with this type actually have a different type which must + * be obtained from their delta base object. Delta objects store only the + * changes needed to apply to the base object in order to recover the + * original object. + * <p> + * A reference delta uses a full object id (hash) to reference the delta + * base. The base object is allowed to be omitted from the packfile, but + * only in the case of a thin pack being transferred over the network. + * <p> + * <b>This constant is fixed and is defined by the Git packfile format.</b> + */ + public static final int OBJ_REF_DELTA = 7; + + /** + * Pack file signature that occurs at file header - identifies file as Git + * packfile formatted. + * <p> + * <b>This constant is fixed and is defined by the Git packfile format.</b> + */ + public static final byte[] PACK_SIGNATURE = { 'P', 'A', 'C', 'K' }; + + /** Native character encoding for commit messages, file names... */ + public static final String CHARACTER_ENCODING = "UTF-8"; + + /** Native character encoding for commit messages, file names... */ + public static final Charset CHARSET; + + /** Default main branch name */ + public static final String MASTER = "master"; + + /** Prefix for branch refs */ + public static final String R_HEADS = "refs/heads/"; + + /** Prefix for remotes refs */ + public static final String R_REMOTES = "refs/remotes/"; + + /** Prefix for tag refs */ + public static final String R_TAGS = "refs/tags/"; + + /** Prefix for any ref */ + public static final String R_REFS = "refs/"; + + /** Logs folder name */ + public static final String LOGS = "logs"; + + /** Info refs folder */ + public static final String INFO_REFS = "info/refs"; + + /** Packed refs file */ + public static final String PACKED_REFS = "packed-refs"; + + /** The environment variable that contains the system user name */ + public static final String OS_USER_NAME_KEY = "user.name"; + + /** The environment variable that contains the author's name */ + public static final String GIT_AUTHOR_NAME_KEY = "GIT_AUTHOR_NAME"; + + /** The environment variable that contains the author's email */ + public static final String GIT_AUTHOR_EMAIL_KEY = "GIT_AUTHOR_EMAIL"; + + /** The environment variable that contains the commiter's name */ + public static final String GIT_COMMITTER_NAME_KEY = "GIT_COMMITTER_NAME"; + + /** The environment variable that contains the commiter's email */ + public static final String GIT_COMMITTER_EMAIL_KEY = "GIT_COMMITTER_EMAIL"; + + /** Default value for the user name if no other information is available */ + public static final String UNKNOWN_USER_DEFAULT = "unknown-user"; + + /** Beginning of the common "Signed-off-by: " commit message line */ + public static final String SIGNED_OFF_BY_TAG = "Signed-off-by: "; + + /** + * Create a new digest function for objects. + * + * @return a new digest object. + * @throws RuntimeException + * this Java virtual machine does not support the required hash + * function. Very unlikely given that JGit uses a hash function + * that is in the Java reference specification. + */ + public static MessageDigest newMessageDigest() { + try { + return MessageDigest.getInstance(HASH_FUNCTION); + } catch (NoSuchAlgorithmException nsae) { + throw new RuntimeException("Required hash function " + + HASH_FUNCTION + " not available.", nsae); + } + } + + /** + * Convert an OBJ_* type constant to a TYPE_* type constant. + * + * @param typeCode the type code, from a pack representation. + * @return the canonical string name of this type. + */ + public static String typeString(final int typeCode) { + switch (typeCode) { + case OBJ_COMMIT: + return TYPE_COMMIT; + case OBJ_TREE: + return TYPE_TREE; + case OBJ_BLOB: + return TYPE_BLOB; + case OBJ_TAG: + return TYPE_TAG; + default: + throw new IllegalArgumentException("Bad object type: " + typeCode); + } + } + + /** + * Convert an OBJ_* type constant to an ASCII encoded string constant. + * <p> + * The ASCII encoded string is often the canonical representation of + * the type within a loose object header, or within a tag header. + * + * @param typeCode the type code, from a pack representation. + * @return the canonical ASCII encoded name of this type. + */ + public static byte[] encodedTypeString(final int typeCode) { + switch (typeCode) { + case OBJ_COMMIT: + return ENCODED_TYPE_COMMIT; + case OBJ_TREE: + return ENCODED_TYPE_TREE; + case OBJ_BLOB: + return ENCODED_TYPE_BLOB; + case OBJ_TAG: + return ENCODED_TYPE_TAG; + default: + throw new IllegalArgumentException("Bad object type: " + typeCode); + } + } + + /** + * Parse an encoded type string into a type constant. + * + * @param id + * object id this type string came from; may be null if that is + * not known at the time the parse is occurring. + * @param typeString + * string version of the type code. + * @param endMark + * character immediately following the type string. Usually ' ' + * (space) or '\n' (line feed). + * @param offset + * position within <code>typeString</code> where the parse + * should start. Updated with the new position (just past + * <code>endMark</code> when the parse is successful. + * @return a type code constant (one of {@link #OBJ_BLOB}, + * {@link #OBJ_COMMIT}, {@link #OBJ_TAG}, {@link #OBJ_TREE}. + * @throws CorruptObjectException + * there is no valid type identified by <code>typeString</code>. + */ + public static int decodeTypeString(final AnyObjectId id, + final byte[] typeString, final byte endMark, + final MutableInteger offset) throws CorruptObjectException { + try { + int position = offset.value; + switch (typeString[position]) { + case 'b': + if (typeString[position + 1] != 'l' + || typeString[position + 2] != 'o' + || typeString[position + 3] != 'b' + || typeString[position + 4] != endMark) + throw new CorruptObjectException(id, "invalid type"); + offset.value = position + 5; + return Constants.OBJ_BLOB; + + case 'c': + if (typeString[position + 1] != 'o' + || typeString[position + 2] != 'm' + || typeString[position + 3] != 'm' + || typeString[position + 4] != 'i' + || typeString[position + 5] != 't' + || typeString[position + 6] != endMark) + throw new CorruptObjectException(id, "invalid type"); + offset.value = position + 7; + return Constants.OBJ_COMMIT; + + case 't': + switch (typeString[position + 1]) { + case 'a': + if (typeString[position + 2] != 'g' + || typeString[position + 3] != endMark) + throw new CorruptObjectException(id, "invalid type"); + offset.value = position + 4; + return Constants.OBJ_TAG; + + case 'r': + if (typeString[position + 2] != 'e' + || typeString[position + 3] != 'e' + || typeString[position + 4] != endMark) + throw new CorruptObjectException(id, "invalid type"); + offset.value = position + 5; + return Constants.OBJ_TREE; + + default: + throw new CorruptObjectException(id, "invalid type"); + } + + default: + throw new CorruptObjectException(id, "invalid type"); + } + } catch (ArrayIndexOutOfBoundsException bad) { + throw new CorruptObjectException(id, "invalid type"); + } + } + + /** + * Convert an integer into its decimal representation. + * + * @param s + * the integer to convert. + * @return a decimal representation of the input integer. The returned array + * is the smallest array that will hold the value. + */ + public static byte[] encodeASCII(final long s) { + return encodeASCII(Long.toString(s)); + } + + /** + * Convert a string to US-ASCII encoding. + * + * @param s + * the string to convert. Must not contain any characters over + * 127 (outside of 7-bit ASCII). + * @return a byte array of the same length as the input string, holding the + * same characters, in the same order. + * @throws IllegalArgumentException + * the input string contains one or more characters outside of + * the 7-bit ASCII character space. + */ + public static byte[] encodeASCII(final String s) { + final byte[] r = new byte[s.length()]; + for (int k = r.length - 1; k >= 0; k--) { + final char c = s.charAt(k); + if (c > 127) + throw new IllegalArgumentException("Not ASCII string: " + s); + r[k] = (byte) c; + } + return r; + } + + /** + * Convert a string to a byte array in the standard character encoding. + * + * @param str + * the string to convert. May contain any Unicode characters. + * @return a byte array representing the requested string, encoded using the + * default character encoding (UTF-8). + * @see #CHARACTER_ENCODING + */ + public static byte[] encode(final String str) { + final ByteBuffer bb = Constants.CHARSET.encode(str); + final int len = bb.limit(); + if (bb.hasArray() && bb.arrayOffset() == 0) { + final byte[] arr = bb.array(); + if (arr.length == len) + return arr; + } + + final byte[] arr = new byte[len]; + bb.get(arr); + return arr; + } + + static { + if (OBJECT_ID_LENGTH != newMessageDigest().getDigestLength()) + throw new LinkageError("Incorrect OBJECT_ID_LENGTH."); + CHARSET = Charset.forName(CHARACTER_ENCODING); + } + + private Constants() { + // Hide the default constructor + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java new file mode 100644 index 0000000000..4d4c3a731f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import static java.util.zip.Deflater.DEFAULT_COMPRESSION; + +import org.eclipse.jgit.lib.Config.SectionParser; + +/** + * This class keeps git repository core parameters. + */ +public class CoreConfig { + /** Key for {@link Config#get(SectionParser)}. */ + public static final Config.SectionParser<CoreConfig> KEY = new SectionParser<CoreConfig>() { + public CoreConfig parse(final Config cfg) { + return new CoreConfig(cfg); + } + }; + + private final int compression; + + private final int packIndexVersion; + + private CoreConfig(final Config rc) { + compression = rc.getInt("core", "compression", DEFAULT_COMPRESSION); + packIndexVersion = rc.getInt("pack", "indexversion", 2); + } + + /** + * @see ObjectWriter + * @return The compression level to use when storing loose objects + */ + public int getCompression() { + return compression; + } + + /** + * @return the preferred pack index file format; 0 for oldest possible. + * @see org.eclipse.jgit.transport.IndexPack + */ + public int getPackIndexVersion() { + return packIndexVersion; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DeltaOfsPackedObjectLoader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DeltaOfsPackedObjectLoader.java new file mode 100644 index 0000000000..9e1b3da198 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DeltaOfsPackedObjectLoader.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; + +import org.eclipse.jgit.errors.CorruptObjectException; + +/** Reads a deltified object which uses an offset to find its base. */ +class DeltaOfsPackedObjectLoader extends DeltaPackedObjectLoader { + private final long deltaBase; + + DeltaOfsPackedObjectLoader(final PackFile pr, + final long dataOffset, final long objectOffset, final int deltaSz, + final long base) { + super(pr, dataOffset, objectOffset, deltaSz); + deltaBase = base; + } + + protected PackedObjectLoader getBaseLoader(final WindowCursor curs) + throws IOException { + return pack.resolveBase(curs, deltaBase); + } + + @Override + public int getRawType() { + return Constants.OBJ_OFS_DELTA; + } + + @Override + public ObjectId getDeltaBase() throws IOException { + final ObjectId id = pack.findObjectForOffset(deltaBase); + if (id == null) + throw new CorruptObjectException( + "Offset-written delta base for object not found in a pack"); + return id; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DeltaPackedObjectLoader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DeltaPackedObjectLoader.java new file mode 100644 index 0000000000..867aadfb29 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DeltaPackedObjectLoader.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.util.zip.DataFormatException; + +import org.eclipse.jgit.errors.CorruptObjectException; + +/** Reader for a deltified object stored in a pack file. */ +abstract class DeltaPackedObjectLoader extends PackedObjectLoader { + private static final int OBJ_COMMIT = Constants.OBJ_COMMIT; + + private final int deltaSize; + + DeltaPackedObjectLoader(final PackFile pr, final long dataOffset, + final long objectOffset, final int deltaSz) { + super(pr, dataOffset, objectOffset); + objectType = -1; + deltaSize = deltaSz; + } + + @Override + public void materialize(final WindowCursor curs) throws IOException { + if (cachedBytes != null) { + return; + } + + if (objectType != OBJ_COMMIT) { + final UnpackedObjectCache.Entry cache = pack.readCache(dataOffset); + if (cache != null) { + curs.release(); + objectType = cache.type; + objectSize = cache.data.length; + cachedBytes = cache.data; + return; + } + } + + try { + final PackedObjectLoader baseLoader = getBaseLoader(curs); + baseLoader.materialize(curs); + cachedBytes = BinaryDelta.apply(baseLoader.getCachedBytes(), + pack.decompress(dataOffset, deltaSize, curs)); + curs.release(); + objectType = baseLoader.getType(); + objectSize = cachedBytes.length; + if (objectType != OBJ_COMMIT) + pack.saveCache(dataOffset, cachedBytes, objectType); + } catch (DataFormatException dfe) { + final CorruptObjectException coe; + coe = new CorruptObjectException("Object at " + dataOffset + " in " + + pack.getPackFile() + " has bad zlib stream"); + coe.initCause(dfe); + throw coe; + } + } + + @Override + public long getRawSize() { + return deltaSize; + } + + /** + * @param curs + * temporary thread storage during data access. + * @return the object loader for the base object + * @throws IOException + */ + protected abstract PackedObjectLoader getBaseLoader(WindowCursor curs) + throws IOException; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DeltaRefPackedObjectLoader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DeltaRefPackedObjectLoader.java new file mode 100644 index 0000000000..3616c41072 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DeltaRefPackedObjectLoader.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; + +import org.eclipse.jgit.errors.MissingObjectException; + +/** Reads a deltified object which uses an {@link ObjectId} to find its base. */ +class DeltaRefPackedObjectLoader extends DeltaPackedObjectLoader { + private final ObjectId deltaBase; + + DeltaRefPackedObjectLoader(final PackFile pr, + final long dataOffset, final long objectOffset, final int deltaSz, + final ObjectId base) { + super(pr, dataOffset, objectOffset, deltaSz); + deltaBase = base; + } + + protected PackedObjectLoader getBaseLoader(final WindowCursor curs) + throws IOException { + final PackedObjectLoader or = pack.get(curs, deltaBase); + if (or == null) + throw new MissingObjectException(deltaBase, "delta base"); + return or; + } + + @Override + public int getRawType() { + return Constants.OBJ_REF_DELTA; + } + + @Override + public ObjectId getDeltaBase() throws IOException { + return deltaBase; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileBasedConfig.java new file mode 100644 index 0000000000..a1843d1189 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileBasedConfig.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2009, JetBrains s.r.o. + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * Copyright (C) 2008, Thad Hughes <thadh@thad.corp.google.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * The configuration file that is stored in the file of the file system. + */ +public class FileBasedConfig extends Config { + private final File configFile; + + /** + * Create a configuration with no default fallback. + * + * @param cfgLocation + * the location of the configuration file on the file system + */ + public FileBasedConfig(File cfgLocation) { + this(null, cfgLocation); + } + + /** + * The constructor + * + * @param base + * the base configuration file + * @param cfgLocation + * the location of the configuration file on the file system + */ + public FileBasedConfig(Config base, File cfgLocation) { + super(base); + configFile = cfgLocation; + } + + /** @return location of the configuration file on disk */ + public final File getFile() { + return configFile; + } + + /** + * Load the configuration as a Git text style configuration file. + * <p> + * If the file does not exist, this configuration is cleared, and thus + * behaves the same as though the file exists, but is empty. + * + * @throws IOException + * the file could not be read (but does exist). + * @throws ConfigInvalidException + * the file is not a properly formatted configuration file. + */ + public void load() throws IOException, ConfigInvalidException { + try { + fromText(RawParseUtils.decode(NB.readFully(getFile()))); + } catch (FileNotFoundException noFile) { + clear(); + } catch (IOException e) { + final IOException e2 = new IOException("Cannot read " + getFile()); + e2.initCause(e); + throw e2; + } catch (ConfigInvalidException e) { + throw new ConfigInvalidException("Cannot read " + getFile(), e); + } + } + + /** + * Save the configuration as a Git text style configuration file. + * <p> + * <b>Warning:</b> Although this method uses the traditional Git file + * locking approach to protect against concurrent writes of the + * configuration file, it does not ensure that the file has not been + * modified since the last read, which means updates performed by other + * objects accessing the same backing file may be lost. + * + * @throws IOException + * the file could not be written. + */ + public void save() throws IOException { + final byte[] out = Constants.encode(toText()); + final LockFile lf = new LockFile(getFile()); + if (!lf.lock()) + throw new IOException("Cannot lock " + getFile()); + try { + lf.write(out); + if (!lf.commit()) + throw new IOException("Cannot commit write to " + getFile()); + } finally { + lf.unlock(); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + getFile().getPath() + "]"; + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java new file mode 100644 index 0000000000..4c3fb6b597 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileMode.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Constants describing various file modes recognized by GIT. + * <p> + * GIT uses a subset of the available UNIX file permission bits. The + * <code>FileMode</code> class provides access to constants defining the modes + * actually used by GIT. + * </p> + */ +public abstract class FileMode { + /** + * Mask to apply to a file mode to obtain its type bits. + * + * @see #TYPE_TREE + * @see #TYPE_SYMLINK + * @see #TYPE_FILE + * @see #TYPE_GITLINK + * @see #TYPE_MISSING + */ + public static final int TYPE_MASK = 0170000; + + /** Bit pattern for {@link #TYPE_MASK} matching {@link #TREE}. */ + public static final int TYPE_TREE = 0040000; + + /** Bit pattern for {@link #TYPE_MASK} matching {@link #SYMLINK}. */ + public static final int TYPE_SYMLINK = 0120000; + + /** Bit pattern for {@link #TYPE_MASK} matching {@link #REGULAR_FILE}. */ + public static final int TYPE_FILE = 0100000; + + /** Bit pattern for {@link #TYPE_MASK} matching {@link #GITLINK}. */ + public static final int TYPE_GITLINK = 0160000; + + /** Bit pattern for {@link #TYPE_MASK} matching {@link #MISSING}. */ + public static final int TYPE_MISSING = 0000000; + + /** Mode indicating an entry is a {@link Tree}. */ + @SuppressWarnings("synthetic-access") + public static final FileMode TREE = new FileMode(TYPE_TREE, + Constants.OBJ_TREE) { + public boolean equals(final int modeBits) { + return (modeBits & TYPE_MASK) == TYPE_TREE; + } + }; + + /** Mode indicating an entry is a {@link SymlinkTreeEntry}. */ + @SuppressWarnings("synthetic-access") + public static final FileMode SYMLINK = new FileMode(TYPE_SYMLINK, + Constants.OBJ_BLOB) { + public boolean equals(final int modeBits) { + return (modeBits & TYPE_MASK) == TYPE_SYMLINK; + } + }; + + /** Mode indicating an entry is a non-executable {@link FileTreeEntry}. */ + @SuppressWarnings("synthetic-access") + public static final FileMode REGULAR_FILE = new FileMode(0100644, + Constants.OBJ_BLOB) { + public boolean equals(final int modeBits) { + return (modeBits & TYPE_MASK) == TYPE_FILE && (modeBits & 0111) == 0; + } + }; + + /** Mode indicating an entry is an executable {@link FileTreeEntry}. */ + @SuppressWarnings("synthetic-access") + public static final FileMode EXECUTABLE_FILE = new FileMode(0100755, + Constants.OBJ_BLOB) { + public boolean equals(final int modeBits) { + return (modeBits & TYPE_MASK) == TYPE_FILE && (modeBits & 0111) != 0; + } + }; + + /** Mode indicating an entry is a submodule commit in another repository. */ + @SuppressWarnings("synthetic-access") + public static final FileMode GITLINK = new FileMode(TYPE_GITLINK, + Constants.OBJ_COMMIT) { + public boolean equals(final int modeBits) { + return (modeBits & TYPE_MASK) == TYPE_GITLINK; + } + }; + + /** Mode indicating an entry is missing during parallel walks. */ + @SuppressWarnings("synthetic-access") + public static final FileMode MISSING = new FileMode(TYPE_MISSING, + Constants.OBJ_BAD) { + public boolean equals(final int modeBits) { + return modeBits == 0; + } + }; + + /** + * Convert a set of mode bits into a FileMode enumerated value. + * + * @param bits + * the mode bits the caller has somehow obtained. + * @return the FileMode instance that represents the given bits. + */ + public static final FileMode fromBits(final int bits) { + switch (bits & TYPE_MASK) { + case TYPE_MISSING: + if (bits == 0) + return MISSING; + break; + case TYPE_TREE: + return TREE; + case TYPE_FILE: + if ((bits & 0111) != 0) + return EXECUTABLE_FILE; + return REGULAR_FILE; + case TYPE_SYMLINK: + return SYMLINK; + case TYPE_GITLINK: + return GITLINK; + } + + return new FileMode(bits, Constants.OBJ_BAD) { + @Override + public boolean equals(final int a) { + return bits == a; + } + }; + } + + private final byte[] octalBytes; + + private final int modeBits; + + private final int objectType; + + private FileMode(int mode, final int expType) { + modeBits = mode; + objectType = expType; + if (mode != 0) { + final byte[] tmp = new byte[10]; + int p = tmp.length; + + while (mode != 0) { + tmp[--p] = (byte) ('0' + (mode & 07)); + mode >>= 3; + } + + octalBytes = new byte[tmp.length - p]; + for (int k = 0; k < octalBytes.length; k++) { + octalBytes[k] = tmp[p + k]; + } + } else { + octalBytes = new byte[] { '0' }; + } + } + + /** + * Test a file mode for equality with this {@link FileMode} object. + * + * @param modebits + * @return true if the mode bits represent the same mode as this object + */ + public abstract boolean equals(final int modebits); + + /** + * Copy this mode as a sequence of octal US-ASCII bytes. + * <p> + * The mode is copied as a sequence of octal digits using the US-ASCII + * character encoding. The sequence does not use a leading '0' prefix to + * indicate octal notation. This method is suitable for generation of a mode + * string within a GIT tree object. + * </p> + * + * @param os + * stream to copy the mode to. + * @throws IOException + * the stream encountered an error during the copy. + */ + public void copyTo(final OutputStream os) throws IOException { + os.write(octalBytes); + } + + /** + * @return the number of bytes written by {@link #copyTo(OutputStream)}. + */ + public int copyToLength() { + return octalBytes.length; + } + + /** + * Get the object type that should appear for this type of mode. + * <p> + * See the object type constants in {@link Constants}. + * + * @return one of the well known object type constants. + */ + public int getObjectType() { + return objectType; + } + + /** Format this mode as an octal string (for debugging only). */ + public String toString() { + return Integer.toOctalString(modeBits); + } + + /** + * @return The mode bits as an integer. + */ + public int getBits() { + return modeBits; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java new file mode 100644 index 0000000000..9ff4deca35 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; + +/** + * A representation of a file (blob) object in a {@link Tree}. + */ +public class FileTreeEntry extends TreeEntry { + private FileMode mode; + + /** + * Constructor for a File (blob) object. + * + * @param parent + * The {@link Tree} holding this object (or null) + * @param id + * the SHA-1 of the blob (or null for a yet unhashed file) + * @param nameUTF8 + * raw object name in the parent tree + * @param execute + * true if the executable flag is set + */ + public FileTreeEntry(final Tree parent, final ObjectId id, + final byte[] nameUTF8, final boolean execute) { + super(parent, id, nameUTF8); + setExecutable(execute); + } + + public FileMode getMode() { + return mode; + } + + /** + * @return true if this file is executable + */ + public boolean isExecutable() { + return getMode().equals(FileMode.EXECUTABLE_FILE); + } + + /** + * @param execute set/reset the executable flag + */ + public void setExecutable(final boolean execute) { + mode = execute ? FileMode.EXECUTABLE_FILE : FileMode.REGULAR_FILE; + } + + /** + * @return an {@link ObjectLoader} that will return the data + * @throws IOException + */ + public ObjectLoader openReader() throws IOException { + return getRepository().openBlob(getId()); + } + + public void accept(final TreeVisitor tv, final int flags) + throws IOException { + if ((MODIFIED_ONLY & flags) == MODIFIED_ONLY && !isModified()) { + return; + } + + tv.visitFile(this); + } + + public String toString() { + final StringBuffer r = new StringBuffer(); + r.append(ObjectId.toString(getId())); + r.append(' '); + r.append(isExecutable() ? 'X' : 'F'); + r.append(' '); + r.append(getFullName()); + return r.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ForceModified.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ForceModified.java new file mode 100644 index 0000000000..6e341d604b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ForceModified.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2009, Jonas Fonseca <fonseca@diku.dk> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; + +/** + * Visitor for marking all nodes of a tree as modified. + */ +public class ForceModified implements TreeVisitor { + public void startVisitTree(final Tree t) throws IOException { + t.setModified(); + } + + public void endVisitTree(final Tree t) throws IOException { + // Nothing to do. + } + + public void visitFile(final FileTreeEntry f) throws IOException { + f.setModified(); + } + + public void visitSymlink(final SymlinkTreeEntry s) throws IOException { + // TODO: handle symlinks. Only problem is that JGit is independent of + // Eclipse + // and Pure Java does not know what to do about symbolic links. + } + + public void visitGitlink(GitlinkTreeEntry s) throws IOException { + // TODO: handle gitlinks. + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitIndex.java new file mode 100644 index 0000000000..d62c9df045 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitIndex.java @@ -0,0 +1,976 @@ +/* + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2007, Robin Rosenberg <me@lathund.dewire.com> + * Copyright (C) 2007-2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Roger C. Soares <rogersoares@intelinet.com.br> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +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.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; +import java.security.MessageDigest; +import java.util.Comparator; +import java.util.Date; +import java.util.Iterator; +import java.util.Map; +import java.util.Stack; +import java.util.TreeMap; + +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * A representation of the Git index. + * + * The index points to the objects currently checked out or in the process of + * being prepared for committing or objects involved in an unfinished merge. + * + * The abstract format is:<br/> path stage flags statdata SHA-1 + * <ul> + * <li>Path is the relative path in the workdir</li> + * <li>stage is 0 (normally), but when + * merging 1 is the common ancestor version, 2 is 'our' version and 3 is 'their' + * version. A fully resolved merge only contains stage 0.</li> + * <li>flags is the object type and information of validity</li> + * <li>statdata is the size of this object and some other file system specifics, + * some of it ignored by JGit</li> + * <li>SHA-1 represents the content of the references object</li> + * </ul> + * + * An index can also contain a tree cache which we ignore for now. We drop the + * tree cache when writing the index. + * + * @deprecated Use {@link DirCache} instead. + */ +public class GitIndex { + + /** Stage 0 represents merged entries. */ + public static final int STAGE_0 = 0; + + private RandomAccessFile cache; + + private File cacheFile; + + // Index is modified + private boolean changed; + + // Stat information updated + private boolean statDirty; + + private Header header; + + private long lastCacheTime; + + private final Repository db; + + private Map<byte[], Entry> entries = new TreeMap<byte[], Entry>(new Comparator<byte[]>() { + public int compare(byte[] o1, byte[] o2) { + for (int i = 0; i < o1.length && i < o2.length; ++i) { + int c = (o1[i] & 0xff) - (o2[i] & 0xff); + if (c != 0) + return c; + } + if (o1.length < o2.length) + return -1; + else if (o1.length > o2.length) + return 1; + return 0; + } + }); + + /** + * Construct a Git index representation. + * @param db + */ + public GitIndex(Repository db) { + this.db = db; + this.cacheFile = new File(db.getDirectory(), "index"); + } + + /** + * @return true if we have modified the index in memory since reading it from disk + */ + public boolean isChanged() { + return changed || statDirty; + } + + /** + * Reread index data from disk if the index file has been changed + * @throws IOException + */ + public void rereadIfNecessary() throws IOException { + if (cacheFile.exists() && cacheFile.lastModified() != lastCacheTime) { + read(); + db.fireIndexChanged(); + } + } + + /** + * Add the content of a file to the index. + * + * @param wd workdir + * @param f the file + * @return a new or updated index entry for the path represented by f + * @throws IOException + */ + public Entry add(File wd, File f) throws IOException { + byte[] key = makeKey(wd, f); + Entry e = entries.get(key); + if (e == null) { + e = new Entry(key, f, 0); + entries.put(key, e); + } else { + e.update(f); + } + return e; + } + + /** + * Add the content of a file to the index. + * + * @param wd + * workdir + * @param f + * the file + * @param content + * content of the file + * @return a new or updated index entry for the path represented by f + * @throws IOException + */ + public Entry add(File wd, File f, byte[] content) throws IOException { + byte[] key = makeKey(wd, f); + Entry e = entries.get(key); + if (e == null) { + e = new Entry(key, f, 0, content); + entries.put(key, e); + } else { + e.update(f, content); + } + return e; + } + + /** + * Remove a path from the index. + * + * @param wd + * workdir + * @param f + * the file whose path shall be removed. + * @return true if such a path was found (and thus removed) + * @throws IOException + */ + public boolean remove(File wd, File f) throws IOException { + byte[] key = makeKey(wd, f); + return entries.remove(key) != null; + } + + /** + * Read the cache file into memory. + * + * @throws IOException + */ + public void read() throws IOException { + changed = false; + statDirty = false; + if (!cacheFile.exists()) { + header = null; + entries.clear(); + lastCacheTime = 0; + return; + } + cache = new RandomAccessFile(cacheFile, "r"); + try { + FileChannel channel = cache.getChannel(); + ByteBuffer buffer = ByteBuffer.allocateDirect((int) cacheFile.length()); + buffer.order(ByteOrder.BIG_ENDIAN); + int j = channel.read(buffer); + if (j != buffer.capacity()) + throw new IOException("Could not read index in one go, only "+j+" out of "+buffer.capacity()+" read"); + buffer.flip(); + header = new Header(buffer); + entries.clear(); + for (int i = 0; i < header.entries; ++i) { + Entry entry = new Entry(buffer); + entries.put(entry.name, entry); + } + lastCacheTime = cacheFile.lastModified(); + } finally { + cache.close(); + } + } + + /** + * Write content of index to disk. + * + * @throws IOException + */ + public void write() throws IOException { + checkWriteOk(); + File tmpIndex = new File(cacheFile.getAbsoluteFile() + ".tmp"); + File lock = new File(cacheFile.getAbsoluteFile() + ".lock"); + if (!lock.createNewFile()) + throw new IOException("Index file is in use"); + try { + FileOutputStream fileOutputStream = new FileOutputStream(tmpIndex); + FileChannel fc = fileOutputStream.getChannel(); + ByteBuffer buf = ByteBuffer.allocate(4096); + MessageDigest newMessageDigest = Constants.newMessageDigest(); + header = new Header(entries); + header.write(buf); + buf.flip(); + newMessageDigest + .update(buf.array(), buf.arrayOffset(), buf.limit()); + fc.write(buf); + buf.flip(); + buf.clear(); + for (Iterator i = entries.values().iterator(); i.hasNext();) { + Entry e = (Entry) i.next(); + e.write(buf); + buf.flip(); + newMessageDigest.update(buf.array(), buf.arrayOffset(), buf + .limit()); + fc.write(buf); + buf.flip(); + buf.clear(); + } + buf.put(newMessageDigest.digest()); + buf.flip(); + fc.write(buf); + fc.close(); + fileOutputStream.close(); + if (cacheFile.exists()) + if (!cacheFile.delete()) + throw new IOException( + "Could not rename delete old index"); + if (!tmpIndex.renameTo(cacheFile)) + throw new IOException( + "Could not rename temporary index file to index"); + changed = false; + statDirty = false; + lastCacheTime = cacheFile.lastModified(); + db.fireIndexChanged(); + } finally { + if (!lock.delete()) + throw new IOException( + "Could not delete lock file. Should not happen"); + if (tmpIndex.exists() && !tmpIndex.delete()) + throw new IOException( + "Could not delete temporary index file. Should not happen"); + } + } + + private void checkWriteOk() throws IOException { + for (Iterator i = entries.values().iterator(); i.hasNext();) { + Entry e = (Entry) i.next(); + if (e.getStage() != 0) { + throw new NotSupportedException("Cannot work with other stages than zero right now. Won't write corrupt index."); + } + } + } + + static boolean File_canExecute( File f){ + return FS.INSTANCE.canExecute(f); + } + + static boolean File_setExecute(File f, boolean value) { + return FS.INSTANCE.setExecute(f, value); + } + + static boolean File_hasExecute() { + return FS.INSTANCE.supportsExecute(); + } + + static byte[] makeKey(File wd, File f) { + if (!f.getPath().startsWith(wd.getPath())) + throw new Error("Path is not in working dir"); + String relName = Repository.stripWorkDir(wd, f); + return Constants.encode(relName); + } + + Boolean filemode; + private boolean config_filemode() { + // temporary til we can actually set parameters. We need to be able + // to change this for testing. + if (filemode != null) + return filemode.booleanValue(); + RepositoryConfig config = db.getConfig(); + return config.getBoolean("core", null, "filemode", true); + } + + /** An index entry */ + public class Entry { + private long ctime; + + private long mtime; + + private int dev; + + private int ino; + + private int mode; + + private int uid; + + private int gid; + + private int size; + + private ObjectId sha1; + + private short flags; + + private byte[] name; + + Entry(byte[] key, File f, int stage) + throws IOException { + ctime = f.lastModified() * 1000000L; + mtime = ctime; // we use same here + dev = -1; + ino = -1; + if (config_filemode() && File_canExecute(f)) + mode = FileMode.EXECUTABLE_FILE.getBits(); + else + mode = FileMode.REGULAR_FILE.getBits(); + uid = -1; + gid = -1; + size = (int) f.length(); + ObjectWriter writer = new ObjectWriter(db); + sha1 = writer.writeBlob(f); + name = key; + flags = (short) ((stage << 12) | name.length); // TODO: fix flags + } + + Entry(byte[] key, File f, int stage, byte[] newContent) + throws IOException { + ctime = f.lastModified() * 1000000L; + mtime = ctime; // we use same here + dev = -1; + ino = -1; + if (config_filemode() && File_canExecute(f)) + mode = FileMode.EXECUTABLE_FILE.getBits(); + else + mode = FileMode.REGULAR_FILE.getBits(); + uid = -1; + gid = -1; + size = newContent.length; + ObjectWriter writer = new ObjectWriter(db); + sha1 = writer.writeBlob(newContent); + name = key; + flags = (short) ((stage << 12) | name.length); // TODO: fix flags + } + + Entry(TreeEntry f, int stage) { + ctime = -1; // hmm + mtime = -1; + dev = -1; + ino = -1; + mode = f.getMode().getBits(); + uid = -1; + gid = -1; + try { + size = (int) db.openBlob(f.getId()).getSize(); + } catch (IOException e) { + e.printStackTrace(); + size = -1; + } + sha1 = f.getId(); + name = Constants.encode(f.getFullName()); + flags = (short) ((stage << 12) | name.length); // TODO: fix flags + } + + Entry(ByteBuffer b) { + int startposition = b.position(); + ctime = b.getInt() * 1000000000L + (b.getInt() % 1000000000L); + mtime = b.getInt() * 1000000000L + (b.getInt() % 1000000000L); + dev = b.getInt(); + ino = b.getInt(); + mode = b.getInt(); + uid = b.getInt(); + gid = b.getInt(); + size = b.getInt(); + byte[] sha1bytes = new byte[Constants.OBJECT_ID_LENGTH]; + b.get(sha1bytes); + sha1 = ObjectId.fromRaw(sha1bytes); + flags = b.getShort(); + name = new byte[flags & 0xFFF]; + b.get(name); + b + .position(startposition + + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2 + + name.length + 8) & ~7)); + } + + /** + * Update this index entry with stat and SHA-1 information if it looks + * like the file has been modified in the workdir. + * + * @param f + * file in work dir + * @return true if a change occurred + * @throws IOException + */ + public boolean update(File f) throws IOException { + long lm = f.lastModified() * 1000000L; + boolean modified = mtime != lm; + mtime = lm; + if (size != f.length()) + modified = true; + if (config_filemode()) { + if (File_canExecute(f) != FileMode.EXECUTABLE_FILE.equals(mode)) { + mode = FileMode.EXECUTABLE_FILE.getBits(); + modified = true; + } + } + if (modified) { + size = (int) f.length(); + ObjectWriter writer = new ObjectWriter(db); + ObjectId newsha1 = writer.writeBlob(f); + if (!newsha1.equals(sha1)) + modified = true; + sha1 = newsha1; + } + return modified; + } + + /** + * Update this index entry with stat and SHA-1 information if it looks + * like the file has been modified in the workdir. + * + * @param f + * file in work dir + * @param newContent + * the new content of the file + * @return true if a change occurred + * @throws IOException + */ + public boolean update(File f, byte[] newContent) throws IOException { + boolean modified = false; + size = newContent.length; + ObjectWriter writer = new ObjectWriter(db); + ObjectId newsha1 = writer.writeBlob(newContent); + if (!newsha1.equals(sha1)) + modified = true; + sha1 = newsha1; + return modified; + } + + void write(ByteBuffer buf) { + int startposition = buf.position(); + buf.putInt((int) (ctime / 1000000000L)); + buf.putInt((int) (ctime % 1000000000L)); + buf.putInt((int) (mtime / 1000000000L)); + buf.putInt((int) (mtime % 1000000000L)); + buf.putInt(dev); + buf.putInt(ino); + buf.putInt(mode); + buf.putInt(uid); + buf.putInt(gid); + buf.putInt(size); + sha1.copyRawTo(buf); + buf.putShort(flags); + buf.put(name); + int end = startposition + + ((8 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 20 + 2 + name.length + 8) & ~7); + int remain = end - buf.position(); + while (remain-- > 0) + buf.put((byte) 0); + } + + /** + * Check if an entry's content is different from the cache, + * + * File status information is used and status is same we + * consider the file identical to the state in the working + * directory. Native git uses more stat fields than we + * have accessible in Java. + * + * @param wd working directory to compare content with + * @return true if content is most likely different. + */ + public boolean isModified(File wd) { + return isModified(wd, false); + } + + /** + * Check if an entry's content is different from the cache, + * + * File status information is used and status is same we + * consider the file identical to the state in the working + * directory. Native git uses more stat fields than we + * have accessible in Java. + * + * @param wd working directory to compare content with + * @param forceContentCheck True if the actual file content + * should be checked if modification time differs. + * + * @return true if content is most likely different. + */ + public boolean isModified(File wd, boolean forceContentCheck) { + + if (isAssumedValid()) + return false; + + if (isUpdateNeeded()) + return true; + + File file = getFile(wd); + if (!file.exists()) + return true; + + // JDK1.6 has file.canExecute + // if (file.canExecute() != FileMode.EXECUTABLE_FILE.equals(mode)) + // return true; + final int exebits = FileMode.EXECUTABLE_FILE.getBits() + ^ FileMode.REGULAR_FILE.getBits(); + + if (config_filemode() && FileMode.EXECUTABLE_FILE.equals(mode)) { + if (!File_canExecute(file)&& File_hasExecute()) + return true; + } else { + if (FileMode.REGULAR_FILE.equals(mode&~exebits)) { + if (!file.isFile()) + return true; + if (config_filemode() && File_canExecute(file) && File_hasExecute()) + return true; + } else { + if (FileMode.SYMLINK.equals(mode)) { + return true; + } else { + if (FileMode.TREE.equals(mode)) { + if (!file.isDirectory()) + return true; + } else { + System.out.println("Does not handle mode "+mode+" ("+file+")"); + return true; + } + } + } + } + + if (file.length() != size) + return true; + + // Git under windows only stores seconds so we round the timestamp + // Java gives us if it looks like the timestamp in index is seconds + // only. Otherwise we compare the timestamp at millisecond prevision. + long javamtime = mtime / 1000000L; + long lastm = file.lastModified(); + if (javamtime % 1000 == 0) + lastm = lastm - lastm % 1000; + if (lastm != javamtime) { + if (!forceContentCheck) + return true; + + try { + InputStream is = new FileInputStream(file); + try { + ObjectWriter objectWriter = new ObjectWriter(db); + ObjectId newId = objectWriter.computeBlobSha1(file + .length(), is); + boolean ret = !newId.equals(sha1); + return ret; + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + is.close(); + } catch (IOException e) { + // can't happen, but if it does we ignore it + e.printStackTrace(); + } + } + } catch (FileNotFoundException e) { + // should not happen because we already checked this + e.printStackTrace(); + throw new Error(e); + } + } + return false; + } + + // for testing + void forceRecheck() { + mtime = -1; + } + + private File getFile(File wd) { + return new File(wd, getName()); + } + + public String toString() { + return getName() + "/SHA-1(" + sha1.name() + ")/M:" + + new Date(ctime / 1000000L) + "/C:" + + new Date(mtime / 1000000L) + "/d" + dev + "/i" + ino + + "/m" + Integer.toString(mode, 8) + "/u" + uid + "/g" + + gid + "/s" + size + "/f" + flags + "/@" + getStage(); + } + + /** + * @return path name for this entry + */ + public String getName() { + return RawParseUtils.decode(name); + } + + /** + * @return path name for this entry as byte array, hopefully UTF-8 encoded + */ + public byte[] getNameUTF8() { + return name; + } + + /** + * @return SHA-1 of the entry managed by this index + */ + public ObjectId getObjectId() { + return sha1; + } + + /** + * @return the stage this entry is in + */ + public int getStage() { + return (flags & 0x3000) >> 12; + } + + /** + * @return size of disk object + */ + public int getSize() { + return size; + } + + /** + * @return true if this entry shall be assumed valid + */ + public boolean isAssumedValid() { + return (flags & 0x8000) != 0; + } + + /** + * @return true if this entry should be checked for changes + */ + public boolean isUpdateNeeded() { + return (flags & 0x4000) != 0; + } + + /** + * Set whether to always assume this entry valid + * + * @param assumeValid true to ignore changes + */ + public void setAssumeValid(boolean assumeValid) { + if (assumeValid) + flags |= 0x8000; + else + flags &= ~0x8000; + } + + /** + * Set whether this entry must be checked + * + * @param updateNeeded + */ + public void setUpdateNeeded(boolean updateNeeded) { + if (updateNeeded) + flags |= 0x4000; + else + flags &= ~0x4000; + } + + /** + * Return raw file mode bits. See {@link FileMode} + * @return file mode bits + */ + public int getModeBits() { + return mode; + } + } + + static class Header { + private int signature; + + private int version; + + int entries; + + Header(ByteBuffer map) throws CorruptObjectException { + read(map); + } + + private void read(ByteBuffer buf) throws CorruptObjectException { + signature = buf.getInt(); + version = buf.getInt(); + entries = buf.getInt(); + if (signature != 0x44495243) + throw new CorruptObjectException("Index signature is invalid: " + + signature); + if (version != 2) + throw new CorruptObjectException( + "Unknown index version (or corrupt index):" + version); + } + + void write(ByteBuffer buf) { + buf.order(ByteOrder.BIG_ENDIAN); + buf.putInt(signature); + buf.putInt(version); + buf.putInt(entries); + } + + Header(Map entryset) { + signature = 0x44495243; + version = 2; + entries = entryset.size(); + } + } + + /** + * Read a Tree recursively into the index + * + * @param t The tree to read + * + * @throws IOException + */ + public void readTree(Tree t) throws IOException { + entries.clear(); + readTree("", t); + } + + void readTree(String prefix, Tree t) throws IOException { + TreeEntry[] members = t.members(); + for (int i = 0; i < members.length; ++i) { + TreeEntry te = members[i]; + String name; + if (prefix.length() > 0) + name = prefix + "/" + te.getName(); + else + name = te.getName(); + if (te instanceof Tree) { + readTree(name, (Tree) te); + } else { + Entry e = new Entry(te, 0); + entries.put(Constants.encode(name), e); + } + } + } + + /** + * Add tree entry to index + * @param te tree entry + * @return new or modified index entry + * @throws IOException + */ + public Entry addEntry(TreeEntry te) throws IOException { + byte[] key = Constants.encode(te.getFullName()); + Entry e = new Entry(te, 0); + entries.put(key, e); + return e; + } + + /** + * Check out content of the content represented by the index + * + * @param wd + * workdir + * @throws IOException + */ + public void checkout(File wd) throws IOException { + for (Entry e : entries.values()) { + if (e.getStage() != 0) + continue; + checkoutEntry(wd, e); + } + } + + /** + * Check out content of the specified index entry + * + * @param wd workdir + * @param e index entry + * @throws IOException + */ + public void checkoutEntry(File wd, Entry e) throws IOException { + ObjectLoader ol = db.openBlob(e.sha1); + byte[] bytes = ol.getBytes(); + File file = new File(wd, e.getName()); + file.delete(); + file.getParentFile().mkdirs(); + FileChannel channel = new FileOutputStream(file).getChannel(); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + int j = channel.write(buffer); + if (j != bytes.length) + throw new IOException("Could not write file " + file); + channel.close(); + if (config_filemode() && File_hasExecute()) { + if (FileMode.EXECUTABLE_FILE.equals(e.mode)) { + if (!File_canExecute(file)) + File_setExecute(file, true); + } else { + if (File_canExecute(file)) + File_setExecute(file, false); + } + } + e.mtime = file.lastModified() * 1000000L; + e.ctime = e.mtime; + } + + /** + * Construct and write tree out of index. + * + * @return SHA-1 of the constructed tree + * + * @throws IOException + */ + public ObjectId writeTree() throws IOException { + checkWriteOk(); + ObjectWriter writer = new ObjectWriter(db); + Tree current = new Tree(db); + Stack<Tree> trees = new Stack<Tree>(); + trees.push(current); + String[] prevName = new String[0]; + for (Entry e : entries.values()) { + if (e.getStage() != 0) + continue; + String[] newName = splitDirPath(e.getName()); + int c = longestCommonPath(prevName, newName); + while (c < trees.size() - 1) { + current.setId(writer.writeTree(current)); + trees.pop(); + current = trees.isEmpty() ? null : (Tree) trees.peek(); + } + while (trees.size() < newName.length) { + if (!current.existsTree(newName[trees.size() - 1])) { + current = new Tree(current, Constants.encode(newName[trees.size() - 1])); + current.getParent().addEntry(current); + trees.push(current); + } else { + current = (Tree) current.findTreeMember(newName[trees + .size() - 1]); + trees.push(current); + } + } + FileTreeEntry ne = new FileTreeEntry(current, e.sha1, + Constants.encode(newName[newName.length - 1]), + (e.mode & FileMode.EXECUTABLE_FILE.getBits()) == FileMode.EXECUTABLE_FILE.getBits()); + current.addEntry(ne); + } + while (!trees.isEmpty()) { + current.setId(writer.writeTree(current)); + trees.pop(); + if (!trees.isEmpty()) + current = trees.peek(); + } + return current.getTreeId(); + } + + String[] splitDirPath(String name) { + String[] tmp = new String[name.length() / 2 + 1]; + int p0 = -1; + int p1; + int c = 0; + while ((p1 = name.indexOf('/', p0 + 1)) != -1) { + tmp[c++] = name.substring(p0 + 1, p1); + p0 = p1; + } + tmp[c++] = name.substring(p0 + 1); + String[] ret = new String[c]; + for (int i = 0; i < c; ++i) { + ret[i] = tmp[i]; + } + return ret; + } + + int longestCommonPath(String[] a, String[] b) { + int i; + for (i = 0; i < a.length && i < b.length; ++i) + if (!a[i].equals(b[i])) + return i; + return i; + } + + /** + * Return the members of the index sorted by the unsigned byte + * values of the path names. + * + * Small beware: Unaccounted for are unmerged entries. You may want + * to abort if members with stage != 0 are found if you are doing + * any updating operations. All stages will be found after one another + * here later. Currently only one stage per name is returned. + * + * @return The index entries sorted + */ + public Entry[] getMembers() { + return entries.values().toArray(new Entry[entries.size()]); + } + + /** + * Look up an entry with the specified path. + * + * @param path + * @return index entry for the path or null if not in index. + * @throws UnsupportedEncodingException + */ + public Entry getEntry(String path) throws UnsupportedEncodingException { + return entries.get(Repository.gitInternalSlash(Constants.encode(path))); + } + + /** + * @return The repository holding this index. + */ + public Repository getRepository() { + return db; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitlinkTreeEntry.java new file mode 100644 index 0000000000..a000759128 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitlinkTreeEntry.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2009, Jonas Fonseca <fonseca@diku.dk> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; + +/** + * A tree entry representing a gitlink entry used for submodules. + * + * Note. Java cannot really handle these as file system objects. + */ +public class GitlinkTreeEntry extends TreeEntry { + private static final long serialVersionUID = 1L; + + /** + * Construct a {@link GitlinkTreeEntry} with the specified name and SHA-1 in + * the specified parent + * + * @param parent + * @param id + * @param nameUTF8 + */ + public GitlinkTreeEntry(final Tree parent, final ObjectId id, + final byte[] nameUTF8) { + super(parent, id, nameUTF8); + } + + public FileMode getMode() { + return FileMode.GITLINK; + } + + public void accept(final TreeVisitor tv, final int flags) + throws IOException { + if ((MODIFIED_ONLY & flags) == MODIFIED_ONLY && !isModified()) { + return; + } + + tv.visitGitlink(this); + } + + @Override + public String toString() { + final StringBuffer r = new StringBuffer(); + r.append(ObjectId.toString(getId())); + r.append(" G "); + r.append(getFullName()); + return r.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexChangedEvent.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexChangedEvent.java new file mode 100644 index 0000000000..3c41e92c40 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexChangedEvent.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +/** + * This class passes information about a changed Git index to a + * {@link RepositoryListener} + * + * Currently only a reference to the repository is passed. + */ +public class IndexChangedEvent extends RepositoryChangedEvent { + IndexChangedEvent(final Repository repository) { + super(repository); + } + + @Override + public String toString() { + return "IndexChangedEvent[" + getRepository() + "]"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java new file mode 100644 index 0000000000..bbcd328b65 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; + +import org.eclipse.jgit.lib.GitIndex.Entry; + +/** + * Compares the Index, a Tree, and the working directory + */ +public class IndexDiff { + private GitIndex index; + private Tree tree; + + /** + * Construct an indexdiff for diffing the workdir against + * the index. + * + * @param repository + * @throws IOException + */ + public IndexDiff(Repository repository) throws IOException { + this.tree = repository.mapTree(Constants.HEAD); + this.index = repository.getIndex(); + } + + /** + * Construct an indexdiff for diffing the workdir against both + * the index and a tree. + * + * @param tree + * @param index + */ + public IndexDiff(Tree tree, GitIndex index) { + this.tree = tree; + this.index = index; + } + + boolean anyChanges = false; + + /** + * Run the diff operation. Until this is called, all lists will be empty + * @return if anything is different between index, tree, and workdir + * @throws IOException + */ + public boolean diff() throws IOException { + final File root = index.getRepository().getWorkDir(); + new IndexTreeWalker(index, tree, root, new AbstractIndexTreeVisitor() { + public void visitEntry(TreeEntry treeEntry, Entry indexEntry, File file) { + if (treeEntry == null) { + added.add(indexEntry.getName()); + anyChanges = true; + } else if (indexEntry == null) { + if (!(treeEntry instanceof Tree)) + removed.add(treeEntry.getFullName()); + anyChanges = true; + } else { + if (!treeEntry.getId().equals(indexEntry.getObjectId())) { + changed.add(indexEntry.getName()); + anyChanges = true; + } + } + + if (indexEntry != null) { + if (!file.exists()) { + missing.add(indexEntry.getName()); + anyChanges = true; + } else { + if (indexEntry.isModified(root, true)) { + modified.add(indexEntry.getName()); + anyChanges = true; + } + } + } + } + }).walk(); + + return anyChanges; + } + + HashSet<String> added = new HashSet<String>(); + HashSet<String> changed = new HashSet<String>(); + HashSet<String> removed = new HashSet<String>(); + HashSet<String> missing = new HashSet<String>(); + HashSet<String> modified = new HashSet<String>(); + + /** + * @return list of files added to the index, not in the tree + */ + public HashSet<String> getAdded() { + return added; + } + + /** + * @return list of files changed from tree to index + */ + public HashSet<String> getChanged() { + return changed; + } + + /** + * @return list of files removed from index, but in tree + */ + public HashSet<String> getRemoved() { + return removed; + } + + /** + * @return list of files in index, but not filesystem + */ + public HashSet<String> getMissing() { + return missing; + } + + /** + * @return list of files on modified on disk relative to the index + */ + public HashSet<String> getModified() { + return modified; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexTreeVisitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexTreeVisitor.java new file mode 100644 index 0000000000..0835b0e52b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexTreeVisitor.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.IOException; + +import org.eclipse.jgit.lib.GitIndex.Entry; + +/** + * Visitor interface for traversing the index and two trees in parallel. + * + * When merging we deal with up to two tree nodes and a base node. Then + * we figure out what to do. + * + * A File argument is supplied to allow us to check for modifications in + * a work tree or update the file. + */ +public interface IndexTreeVisitor { + /** + * Visit a blob, and corresponding tree and index entries. + * + * @param treeEntry + * @param indexEntry + * @param file + * @throws IOException + */ + public void visitEntry(TreeEntry treeEntry, Entry indexEntry, File file) throws IOException; + + /** + * Visit a blob, and corresponding tree nodes and associated index entry. + * + * @param treeEntry + * @param auxEntry + * @param indexEntry + * @param file + * @throws IOException + */ + public void visitEntry(TreeEntry treeEntry, TreeEntry auxEntry, Entry indexEntry, File file) throws IOException; + + /** + * Invoked after handling all child nodes of a tree, during a three way merge + * + * @param tree + * @param auxTree + * @param curDir + * @throws IOException + */ + public void finishVisitTree(Tree tree, Tree auxTree, String curDir) throws IOException; + + /** + * Invoked after handling all child nodes of a tree, during two way merge. + * + * @param tree + * @param i + * @param curDir + * @throws IOException + */ + public void finishVisitTree(Tree tree, int i, String curDir) throws IOException; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexTreeWalker.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexTreeWalker.java new file mode 100644 index 0000000000..22d9584344 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexTreeWalker.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2007-2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.IOException; + +import org.eclipse.jgit.lib.GitIndex.Entry; + +/** + * A class for traversing the index and one or two trees. + * + * A visitor is invoked for executing actions, like figuring out how to merge. + */ +public class IndexTreeWalker { + private final Tree mainTree; + private final Tree newTree; + private final File root; + private final IndexTreeVisitor visitor; + private boolean threeTrees; + + /** + * Construct a walker for the index and one tree. + * + * @param index + * @param tree + * @param root + * @param visitor + */ + public IndexTreeWalker(GitIndex index, Tree tree, File root, IndexTreeVisitor visitor) { + this.mainTree = tree; + this.root = root; + this.visitor = visitor; + this.newTree = null; + + threeTrees = false; + + indexMembers = index.getMembers(); + } + + /** + * Construct a walker for the index and two trees. + * + * @param index + * @param mainTree + * @param newTree + * @param root + * @param visitor + */ + public IndexTreeWalker(GitIndex index, Tree mainTree, Tree newTree, File root, IndexTreeVisitor visitor) { + this.mainTree = mainTree; + this.newTree = newTree; + this.root = root; + this.visitor = visitor; + + threeTrees = true; + + indexMembers = index.getMembers(); + } + + Entry[] indexMembers; + int indexCounter = 0; + + /** + * Actually walk the index tree + * + * @throws IOException + */ + public void walk() throws IOException { + walk(mainTree, newTree); + } + + private void walk(Tree tree, Tree auxTree) throws IOException { + TreeIterator mi = new TreeIterator(tree, TreeIterator.Order.POSTORDER); + TreeIterator ai = new TreeIterator(auxTree, TreeIterator.Order.POSTORDER); + TreeEntry m = mi.hasNext() ? mi.next() : null; + TreeEntry a = ai.hasNext() ? ai.next() : null; + int curIndexPos = indexCounter; + Entry i = indexCounter < indexMembers.length ? indexMembers[indexCounter++] : null; + while (m != null || a != null || i != null) { + int cmpma = compare(m, a); + int cmpmi = compare(m, i); + int cmpai = compare(a, i); + + TreeEntry pm = cmpma <= 0 && cmpmi <= 0 ? m : null; + TreeEntry pa = cmpma >= 0 && cmpai <= 0 ? a : null; + Entry pi = cmpmi >= 0 && cmpai >= 0 ? i : null; + + if (pi != null) + visitEntry(pm, pa, pi); + else + finishVisitTree(pm, pa, curIndexPos); + + if (pm != null) m = mi.hasNext() ? mi.next() : null; + if (pa != null) a = ai.hasNext() ? ai.next() : null; + if (pi != null) i = indexCounter < indexMembers.length ? indexMembers[indexCounter++] : null; + } + } + + private void visitEntry(TreeEntry t1, TreeEntry t2, + Entry i) throws IOException { + + assert t1 != null || t2 != null || i != null : "Needs at least one entry"; + assert root != null : "Needs workdir"; + + if (t1 != null && t1.getParent() == null) + t1 = null; + if (t2 != null && t2.getParent() == null) + t2 = null; + + File f = null; + if (i != null) + f = new File(root, i.getName()); + else if (t1 != null) + f = new File(root, t1.getFullName()); + else if (t2 != null) + f = new File(root, t2.getFullName()); + + if (t1 != null || t2 != null || i != null) + if (threeTrees) + visitor.visitEntry(t1, t2, i, f); + else + visitor.visitEntry(t1, i, f); + } + + private void finishVisitTree(TreeEntry t1, TreeEntry t2, int curIndexPos) + throws IOException { + + assert t1 != null || t2 != null : "Needs at least one entry"; + assert root != null : "Needs workdir"; + + if (t1 != null && t1.getParent() == null) + t1 = null; + if (t2 != null && t2.getParent() == null) + t2 = null; + + File f = null; + String c= null; + if (t1 != null) { + c = t1.getFullName(); + f = new File(root, c); + } else if (t2 != null) { + c = t2.getFullName(); + f = new File(root, c); + } + if (t1 instanceof Tree || t2 instanceof Tree) + if (threeTrees) + visitor.finishVisitTree((Tree)t1, (Tree)t2, c); + else + visitor.finishVisitTree((Tree)t1, indexCounter - curIndexPos, c); + else if (t1 != null || t2 != null) + if (threeTrees) + visitor.visitEntry(t1, t2, null, f); + else + visitor.visitEntry(t1, null, f); + } + + static boolean lt(TreeEntry h, Entry i) { + return compare(h, i) < 0; + } + + static boolean lt(Entry i, TreeEntry t) { + return compare(t, i) > 0; + } + + static boolean lt(TreeEntry h, TreeEntry m) { + return compare(h, m) < 0; + } + + static boolean eq(TreeEntry t1, TreeEntry t2) { + return compare(t1, t2) == 0; + } + + static boolean eq(TreeEntry t1, Entry e) { + return compare(t1, e) == 0; + } + + static int compare(TreeEntry t, Entry i) { + if (t == null && i == null) + return 0; + if (t == null) + return 1; + if (i == null) + return -1; + return Tree.compareNames(t.getFullNameUTF8(), i.getNameUTF8(), TreeEntry.lastChar(t), TreeEntry.lastChar(i)); + } + + static int compare(TreeEntry t1, TreeEntry t2) { + if (t1 != null && t1.getParent() == null && t2 != null && t2.getParent() == null) + return 0; + if (t1 != null && t1.getParent() == null) + return -1; + if (t2 != null && t2.getParent() == null) + return 1; + + if (t1 == null && t2 == null) + return 0; + if (t1 == null) + return 1; + if (t2 == null) + return -1; + return Tree.compareNames(t1.getFullNameUTF8(), t2.getFullNameUTF8(), TreeEntry.lastChar(t1), TreeEntry.lastChar(t2)); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/InflaterCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/InflaterCache.java new file mode 100644 index 0000000000..9705898842 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/InflaterCache.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.util.zip.Inflater; + +/** Creates zlib based inflaters as necessary for object decompression. */ +public class InflaterCache { + private static final int SZ = 4; + + private static final Inflater[] inflaterCache; + + private static int openInflaterCount; + + static { + inflaterCache = new Inflater[SZ]; + } + + /** + * Obtain an Inflater for decompression. + * <p> + * Inflaters obtained through this cache should be returned (if possible) by + * {@link #release(Inflater)} to avoid garbage collection and reallocation. + * + * @return an available inflater. Never null. + */ + public static Inflater get() { + final Inflater r = getImpl(); + return r != null ? r : new Inflater(false); + } + + private synchronized static Inflater getImpl() { + if (openInflaterCount > 0) { + final Inflater r = inflaterCache[--openInflaterCount]; + inflaterCache[openInflaterCount] = null; + return r; + } + return null; + } + + /** + * Release an inflater previously obtained from this cache. + * + * @param i + * the inflater to return. May be null, in which case this method + * does nothing. + */ + public static void release(final Inflater i) { + if (i != null) { + i.reset(); + if (releaseImpl(i)) + i.end(); + } + } + + private static synchronized boolean releaseImpl(final Inflater i) { + if (openInflaterCount < SZ) { + inflaterCache[openInflaterCount++] = i; + return false; + } + return true; + } + + private InflaterCache() { + throw new UnsupportedOperationException(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/LockFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/LockFile.java new file mode 100644 index 0000000000..bf0036beec --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/LockFile.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; + +/** + * Git style file locking and replacement. + * <p> + * To modify a ref file Git tries to use an atomic update approach: we write the + * new data into a brand new file, then rename it in place over the old name. + * This way we can just delete the temporary file if anything goes wrong, and + * nothing has been damaged. To coordinate access from multiple processes at + * once Git tries to atomically create the new temporary file under a well-known + * name. + */ +public class LockFile { + private final File ref; + + private final File lck; + + private FileLock fLck; + + private boolean haveLck; + + private FileOutputStream os; + + private boolean needStatInformation; + + private long commitLastModified; + + /** + * Create a new lock for any file. + * + * @param f + * the file that will be locked. + */ + public LockFile(final File f) { + ref = f; + lck = new File(ref.getParentFile(), ref.getName() + ".lock"); + } + + /** + * Try to establish the lock. + * + * @return true if the lock is now held by the caller; false if it is held + * by someone else. + * @throws IOException + * the temporary output file could not be created. The caller + * does not hold the lock. + */ + public boolean lock() throws IOException { + lck.getParentFile().mkdirs(); + if (lck.createNewFile()) { + haveLck = true; + try { + os = new FileOutputStream(lck); + try { + fLck = os.getChannel().tryLock(); + if (fLck == null) + throw new OverlappingFileLockException(); + } catch (OverlappingFileLockException ofle) { + // We cannot use unlock() here as this file is not + // held by us, but we thought we created it. We must + // not delete it, as it belongs to some other process. + // + haveLck = false; + try { + os.close(); + } catch (IOException ioe) { + // Fail by returning haveLck = false. + } + os = null; + } + } catch (IOException ioe) { + unlock(); + throw ioe; + } + } + return haveLck; + } + + /** + * Try to establish the lock for appending. + * + * @return true if the lock is now held by the caller; false if it is held + * by someone else. + * @throws IOException + * the temporary output file could not be created. The caller + * does not hold the lock. + */ + public boolean lockForAppend() throws IOException { + if (!lock()) + return false; + copyCurrentContent(); + return true; + } + + /** + * Copy the current file content into the temporary file. + * <p> + * This method saves the current file content by inserting it into the + * temporary file, so that the caller can safely append rather than replace + * the primary file. + * <p> + * This method does nothing if the current file does not exist, or exists + * but is empty. + * + * @throws IOException + * the temporary file could not be written, or a read error + * occurred while reading from the current file. The lock is + * released before throwing the underlying IO exception to the + * caller. + * @throws RuntimeException + * the temporary file could not be written. The lock is released + * before throwing the underlying exception to the caller. + */ + public void copyCurrentContent() throws IOException { + requireLock(); + try { + final FileInputStream fis = new FileInputStream(ref); + try { + final byte[] buf = new byte[2048]; + int r; + while ((r = fis.read(buf)) >= 0) + os.write(buf, 0, r); + } finally { + fis.close(); + } + } catch (FileNotFoundException fnfe) { + // Don't worry about a file that doesn't exist yet, it + // conceptually has no current content to copy. + // + } catch (IOException ioe) { + unlock(); + throw ioe; + } catch (RuntimeException ioe) { + unlock(); + throw ioe; + } catch (Error ioe) { + unlock(); + throw ioe; + } + } + + /** + * Write an ObjectId and LF to the temporary file. + * + * @param id + * the id to store in the file. The id will be written in hex, + * followed by a sole LF. + * @throws IOException + * the temporary file could not be written. The lock is released + * before throwing the underlying IO exception to the caller. + * @throws RuntimeException + * the temporary file could not be written. The lock is released + * before throwing the underlying exception to the caller. + */ + public void write(final ObjectId id) throws IOException { + requireLock(); + try { + final BufferedOutputStream b; + b = new BufferedOutputStream(os, Constants.OBJECT_ID_LENGTH * 2 + 1); + id.copyTo(b); + b.write('\n'); + b.flush(); + fLck.release(); + b.close(); + os = null; + } catch (IOException ioe) { + unlock(); + throw ioe; + } catch (RuntimeException ioe) { + unlock(); + throw ioe; + } catch (Error ioe) { + unlock(); + throw ioe; + } + } + + /** + * Write arbitrary data to the temporary file. + * + * @param content + * the bytes to store in the temporary file. No additional bytes + * are added, so if the file must end with an LF it must appear + * at the end of the byte array. + * @throws IOException + * the temporary file could not be written. The lock is released + * before throwing the underlying IO exception to the caller. + * @throws RuntimeException + * the temporary file could not be written. The lock is released + * before throwing the underlying exception to the caller. + */ + public void write(final byte[] content) throws IOException { + requireLock(); + try { + os.write(content); + os.flush(); + fLck.release(); + os.close(); + os = null; + } catch (IOException ioe) { + unlock(); + throw ioe; + } catch (RuntimeException ioe) { + unlock(); + throw ioe; + } catch (Error ioe) { + unlock(); + throw ioe; + } + } + + /** + * Obtain the direct output stream for this lock. + * <p> + * The stream may only be accessed once, and only after {@link #lock()} has + * been successfully invoked and returned true. Callers must close the + * stream prior to calling {@link #commit()} to commit the change. + * + * @return a stream to write to the new file. The stream is unbuffered. + */ + public OutputStream getOutputStream() { + requireLock(); + return new OutputStream() { + @Override + public void write(final byte[] b, final int o, final int n) + throws IOException { + os.write(b, o, n); + } + + @Override + public void write(final byte[] b) throws IOException { + os.write(b); + } + + @Override + public void write(final int b) throws IOException { + os.write(b); + } + + @Override + public void flush() throws IOException { + os.flush(); + } + + @Override + public void close() throws IOException { + try { + os.flush(); + fLck.release(); + os.close(); + os = null; + } catch (IOException ioe) { + unlock(); + throw ioe; + } catch (RuntimeException ioe) { + unlock(); + throw ioe; + } catch (Error ioe) { + unlock(); + throw ioe; + } + } + }; + } + + private void requireLock() { + if (os == null) { + unlock(); + throw new IllegalStateException("Lock on " + ref + " not held."); + } + } + + /** + * Request that {@link #commit()} remember modification time. + * + * @param on + * true if the commit method must remember the modification time. + */ + public void setNeedStatInformation(final boolean on) { + needStatInformation = on; + } + + /** + * Commit this change and release the lock. + * <p> + * If this method fails (returns false) the lock is still released. + * + * @return true if the commit was successful and the file contains the new + * data; false if the commit failed and the file remains with the + * old data. + * @throws IllegalStateException + * the lock is not held. + */ + public boolean commit() { + if (os != null) { + unlock(); + throw new IllegalStateException("Lock on " + ref + " not closed."); + } + + saveStatInformation(); + if (lck.renameTo(ref)) + return true; + if (!ref.exists() || ref.delete()) + if (lck.renameTo(ref)) + return true; + unlock(); + return false; + } + + private void saveStatInformation() { + if (needStatInformation) + commitLastModified = lck.lastModified(); + } + + /** + * Get the modification time of the output file when it was committed. + * + * @return modification time of the lock file right before we committed it. + */ + public long getCommitLastModified() { + return commitLastModified; + } + + /** + * Unlock this file and abort this change. + * <p> + * The temporary file (if created) is deleted before returning. + */ + public void unlock() { + if (os != null) { + if (fLck != null) { + try { + fLck.release(); + } catch (IOException ioe) { + // Huh? + } + fLck = null; + } + try { + os.close(); + } catch (IOException ioe) { + // Ignore this + } + os = null; + } + + if (haveLck) { + haveLck = false; + lck.delete(); + } + } + + @Override + public String toString() { + return "LockFile[" + lck + ", haveLck=" + haveLck + "]"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/MutableObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/MutableObjectId.java new file mode 100644 index 0000000000..478f8ba618 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/MutableObjectId.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2009, Jonas Fonseca <fonseca@diku.dk> + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import org.eclipse.jgit.errors.InvalidObjectIdException; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * A mutable SHA-1 abstraction. + */ +public class MutableObjectId extends AnyObjectId { + /** + * Empty constructor. Initialize object with default (zeros) value. + */ + public MutableObjectId() { + super(); + } + + /** + * Copying constructor. + * + * @param src + * original entry, to copy id from + */ + MutableObjectId(MutableObjectId src) { + this.w1 = src.w1; + this.w2 = src.w2; + this.w3 = src.w3; + this.w4 = src.w4; + this.w5 = src.w5; + } + + /** Make this id match {@link ObjectId#zeroId()}. */ + public void clear() { + w1 = 0; + w2 = 0; + w3 = 0; + w4 = 0; + w5 = 0; + } + + /** + * Convert an ObjectId from raw binary representation. + * + * @param bs + * the raw byte buffer to read from. At least 20 bytes must be + * available within this byte array. + */ + public void fromRaw(final byte[] bs) { + fromRaw(bs, 0); + } + + /** + * Convert an ObjectId from raw binary representation. + * + * @param bs + * the raw byte buffer to read from. At least 20 bytes after p + * must be available within this byte array. + * @param p + * position to read the first byte of data from. + */ + public void fromRaw(final byte[] bs, final int p) { + w1 = NB.decodeInt32(bs, p); + w2 = NB.decodeInt32(bs, p + 4); + w3 = NB.decodeInt32(bs, p + 8); + w4 = NB.decodeInt32(bs, p + 12); + w5 = NB.decodeInt32(bs, p + 16); + } + + /** + * Convert an ObjectId from binary representation expressed in integers. + * + * @param ints + * the raw int buffer to read from. At least 5 integers must be + * available within this integers array. + */ + public void fromRaw(final int[] ints) { + fromRaw(ints, 0); + } + + /** + * Convert an ObjectId from binary representation expressed in integers. + * + * @param ints + * the raw int buffer to read from. At least 5 integers after p + * must be available within this integers array. + * @param p + * position to read the first integer of data from. + * + */ + public void fromRaw(final int[] ints, final int p) { + w1 = ints[p]; + w2 = ints[p + 1]; + w3 = ints[p + 2]; + w4 = ints[p + 3]; + w5 = ints[p + 4]; + } + + /** + * Convert an ObjectId from hex characters (US-ASCII). + * + * @param buf + * the US-ASCII buffer to read from. At least 40 bytes after + * offset must be available within this byte array. + * @param offset + * position to read the first character from. + */ + public void fromString(final byte[] buf, final int offset) { + fromHexString(buf, offset); + } + + /** + * Convert an ObjectId from hex characters. + * + * @param str + * the string to read from. Must be 40 characters long. + */ + public void fromString(final String str) { + if (str.length() != STR_LEN) + throw new IllegalArgumentException("Invalid id: " + str); + fromHexString(Constants.encodeASCII(str), 0); + } + + private void fromHexString(final byte[] bs, int p) { + try { + w1 = RawParseUtils.parseHexInt32(bs, p); + w2 = RawParseUtils.parseHexInt32(bs, p + 8); + w3 = RawParseUtils.parseHexInt32(bs, p + 16); + w4 = RawParseUtils.parseHexInt32(bs, p + 24); + w5 = RawParseUtils.parseHexInt32(bs, p + 32); + } catch (ArrayIndexOutOfBoundsException e1) { + throw new InvalidObjectIdException(bs, p, STR_LEN); + } + } + + @Override + public ObjectId toObjectId() { + return new ObjectId(this); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/NullProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/NullProgressMonitor.java new file mode 100644 index 0000000000..d05c8c6b01 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/NullProgressMonitor.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2009, Alex Blewitt <alex.blewitt@gmail.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +/** + * A NullProgressMonitor does not report progress anywhere. + */ +public class NullProgressMonitor implements ProgressMonitor { + /** Immutable instance of a null progress monitor. */ + public static final NullProgressMonitor INSTANCE = new NullProgressMonitor(); + + private NullProgressMonitor() { + // Do not let others instantiate + } + + public void start(int totalTasks) { + // Do not report. + } + + public void beginTask(String title, int totalWork) { + // Do not report. + } + + public void update(int completed) { + // Do not report. + } + + public boolean isCancelled() { + return false; + } + + public void endTask() { + // Do not report. + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java new file mode 100644 index 0000000000..9cf1643db5 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import static org.eclipse.jgit.util.RawParseUtils.match; +import static org.eclipse.jgit.util.RawParseUtils.nextLF; +import static org.eclipse.jgit.util.RawParseUtils.parseBase10; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.util.MutableInteger; + +/** + * Verifies that an object is formatted correctly. + * <p> + * Verifications made by this class only check that the fields of an object are + * formatted correctly. The ObjectId checksum of the object is not verified, and + * connectivity links between objects are also not verified. Its assumed that + * the caller can provide both of these validations on its own. + * <p> + * Instances of this class are not thread safe, but they may be reused to + * perform multiple object validations. + */ +public class ObjectChecker { + /** Header "tree " */ + public static final byte[] tree = Constants.encodeASCII("tree "); + + /** Header "parent " */ + public static final byte[] parent = Constants.encodeASCII("parent "); + + /** Header "author " */ + public static final byte[] author = Constants.encodeASCII("author "); + + /** Header "committer " */ + public static final byte[] committer = Constants.encodeASCII("committer "); + + /** Header "encoding " */ + public static final byte[] encoding = Constants.encodeASCII("encoding "); + + /** Header "object " */ + public static final byte[] object = Constants.encodeASCII("object "); + + /** Header "type " */ + public static final byte[] type = Constants.encodeASCII("type "); + + /** Header "tag " */ + public static final byte[] tag = Constants.encodeASCII("tag "); + + /** Header "tagger " */ + public static final byte[] tagger = Constants.encodeASCII("tagger "); + + private final MutableObjectId tempId = new MutableObjectId(); + + private final MutableInteger ptrout = new MutableInteger(); + + /** + * Check an object for parsing errors. + * + * @param objType + * type of the object. Must be a valid object type code in + * {@link Constants}. + * @param raw + * the raw data which comprises the object. This should be in the + * canonical format (that is the format used to generate the + * ObjectId of the object). The array is never modified. + * @throws CorruptObjectException + * if an error is identified. + */ + public void check(final int objType, final byte[] raw) + throws CorruptObjectException { + switch (objType) { + case Constants.OBJ_COMMIT: + checkCommit(raw); + break; + case Constants.OBJ_TAG: + checkTag(raw); + break; + case Constants.OBJ_TREE: + checkTree(raw); + break; + case Constants.OBJ_BLOB: + checkBlob(raw); + break; + default: + throw new CorruptObjectException("Invalid object type: " + objType); + } + } + + private int id(final byte[] raw, final int ptr) { + try { + tempId.fromString(raw, ptr); + return ptr + AnyObjectId.STR_LEN; + } catch (IllegalArgumentException e) { + return -1; + } + } + + private int personIdent(final byte[] raw, int ptr) { + final int emailB = nextLF(raw, ptr, '<'); + if (emailB == ptr || raw[emailB - 1] != '<') + return -1; + + final int emailE = nextLF(raw, emailB, '>'); + if (emailE == emailB || raw[emailE - 1] != '>') + return -1; + if (emailE == raw.length || raw[emailE] != ' ') + return -1; + + parseBase10(raw, emailE + 1, ptrout); // when + ptr = ptrout.value; + if (emailE + 1 == ptr) + return -1; + if (ptr == raw.length || raw[ptr] != ' ') + return -1; + + parseBase10(raw, ptr + 1, ptrout); // tz offset + if (ptr + 1 == ptrout.value) + return -1; + return ptrout.value; + } + + /** + * Check a commit for errors. + * + * @param raw + * the commit data. The array is never modified. + * @throws CorruptObjectException + * if any error was detected. + */ + public void checkCommit(final byte[] raw) throws CorruptObjectException { + int ptr = 0; + + if ((ptr = match(raw, ptr, tree)) < 0) + throw new CorruptObjectException("no tree header"); + if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n') + throw new CorruptObjectException("invalid tree"); + + while (match(raw, ptr, parent) >= 0) { + ptr += parent.length; + if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n') + throw new CorruptObjectException("invalid parent"); + } + + if ((ptr = match(raw, ptr, author)) < 0) + throw new CorruptObjectException("no author"); + if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n') + throw new CorruptObjectException("invalid author"); + + if ((ptr = match(raw, ptr, committer)) < 0) + throw new CorruptObjectException("no committer"); + if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n') + throw new CorruptObjectException("invalid committer"); + } + + /** + * Check an annotated tag for errors. + * + * @param raw + * the tag data. The array is never modified. + * @throws CorruptObjectException + * if any error was detected. + */ + public void checkTag(final byte[] raw) throws CorruptObjectException { + int ptr = 0; + + if ((ptr = match(raw, ptr, object)) < 0) + throw new CorruptObjectException("no object header"); + if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n') + throw new CorruptObjectException("invalid object"); + + if ((ptr = match(raw, ptr, type)) < 0) + throw new CorruptObjectException("no type header"); + ptr = nextLF(raw, ptr); + + if ((ptr = match(raw, ptr, tag)) < 0) + throw new CorruptObjectException("no tag header"); + ptr = nextLF(raw, ptr); + + if ((ptr = match(raw, ptr, tagger)) < 0) + throw new CorruptObjectException("no tagger header"); + if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n') + throw new CorruptObjectException("invalid tagger"); + } + + private static int lastPathChar(final int mode) { + return FileMode.TREE.equals(mode) ? '/' : '\0'; + } + + private static int pathCompare(final byte[] raw, int aPos, final int aEnd, + final int aMode, int bPos, final int bEnd, final int bMode) { + while (aPos < aEnd && bPos < bEnd) { + final int cmp = (raw[aPos++] & 0xff) - (raw[bPos++] & 0xff); + if (cmp != 0) + return cmp; + } + + if (aPos < aEnd) + return (raw[aPos] & 0xff) - lastPathChar(bMode); + if (bPos < bEnd) + return lastPathChar(aMode) - (raw[bPos] & 0xff); + return 0; + } + + private static boolean duplicateName(final byte[] raw, + final int thisNamePos, final int thisNameEnd) { + final int sz = raw.length; + int nextPtr = thisNameEnd + 1 + Constants.OBJECT_ID_LENGTH; + for (;;) { + int nextMode = 0; + for (;;) { + if (nextPtr >= sz) + return false; + final byte c = raw[nextPtr++]; + if (' ' == c) + break; + nextMode <<= 3; + nextMode += c - '0'; + } + + final int nextNamePos = nextPtr; + for (;;) { + if (nextPtr == sz) + return false; + final byte c = raw[nextPtr++]; + if (c == 0) + break; + } + if (nextNamePos + 1 == nextPtr) + return false; + + final int cmp = pathCompare(raw, thisNamePos, thisNameEnd, + FileMode.TREE.getBits(), nextNamePos, nextPtr - 1, nextMode); + if (cmp < 0) + return false; + else if (cmp == 0) + return true; + + nextPtr += Constants.OBJECT_ID_LENGTH; + } + } + + /** + * Check a canonical formatted tree for errors. + * + * @param raw + * the raw tree data. The array is never modified. + * @throws CorruptObjectException + * if any error was detected. + */ + public void checkTree(final byte[] raw) throws CorruptObjectException { + final int sz = raw.length; + int ptr = 0; + int lastNameB = 0, lastNameE = 0, lastMode = 0; + + while (ptr < sz) { + int thisMode = 0; + for (;;) { + if (ptr == sz) + throw new CorruptObjectException("truncated in mode"); + final byte c = raw[ptr++]; + if (' ' == c) + break; + if (c < '0' || c > '7') + throw new CorruptObjectException("invalid mode character"); + if (thisMode == 0 && c == '0') + throw new CorruptObjectException("mode starts with '0'"); + thisMode <<= 3; + thisMode += c - '0'; + } + + if (FileMode.fromBits(thisMode).getObjectType() == Constants.OBJ_BAD) + throw new CorruptObjectException("invalid mode " + thisMode); + + final int thisNameB = ptr; + for (;;) { + if (ptr == sz) + throw new CorruptObjectException("truncated in name"); + final byte c = raw[ptr++]; + if (c == 0) + break; + if (c == '/') + throw new CorruptObjectException("name contains '/'"); + } + if (thisNameB + 1 == ptr) + throw new CorruptObjectException("zero length name"); + if (raw[thisNameB] == '.') { + final int nameLen = (ptr - 1) - thisNameB; + if (nameLen == 1) + throw new CorruptObjectException("invalid name '.'"); + if (nameLen == 2 && raw[thisNameB + 1] == '.') + throw new CorruptObjectException("invalid name '..'"); + } + if (duplicateName(raw, thisNameB, ptr - 1)) + throw new CorruptObjectException("duplicate entry names"); + + if (lastNameB != 0) { + final int cmp = pathCompare(raw, lastNameB, lastNameE, + lastMode, thisNameB, ptr - 1, thisMode); + if (cmp > 0) + throw new CorruptObjectException("incorrectly sorted"); + } + + lastNameB = thisNameB; + lastNameE = ptr - 1; + lastMode = thisMode; + + ptr += Constants.OBJECT_ID_LENGTH; + if (ptr > sz) + throw new CorruptObjectException("truncated in object id"); + } + } + + /** + * Check a blob for errors. + * + * @param raw + * the blob data. The array is never modified. + * @throws CorruptObjectException + * if any error was detected. + */ + public void checkBlob(final byte[] raw) throws CorruptObjectException { + // We can always assume the blob is valid. + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java new file mode 100644 index 0000000000..21b7b9dc93 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Abstraction of arbitrary object storage. + * <p> + * An object database stores one or more Git objects, indexed by their unique + * {@link ObjectId}. Optionally an object database can reference one or more + * alternates; other ObjectDatabase instances that are searched in addition to + * the current database. + * <p> + * Databases are usually divided into two halves: a half that is considered to + * be fast to search, and a half that is considered to be slow to search. When + * alternates are present the fast half is fully searched (recursively through + * all alternates) before the slow half is considered. + */ +public abstract class ObjectDatabase { + /** Constant indicating no alternate databases exist. */ + protected static final ObjectDatabase[] NO_ALTERNATES = {}; + + private final AtomicReference<ObjectDatabase[]> alternates; + + /** Initialize a new database instance for access. */ + protected ObjectDatabase() { + alternates = new AtomicReference<ObjectDatabase[]>(); + } + + /** + * Does this database exist yet? + * + * @return true if this database is already created; false if the caller + * should invoke {@link #create()} to create this database location. + */ + public boolean exists() { + return true; + } + + /** + * Initialize a new object database at this location. + * + * @throws IOException + * the database could not be created. + */ + public void create() throws IOException { + // Assume no action is required. + } + + /** + * Close any resources held by this database and its active alternates. + */ + public final void close() { + closeSelf(); + closeAlternates(); + } + + /** + * Close any resources held by this database only; ignoring alternates. + * <p> + * To fully close this database and its referenced alternates, the caller + * should instead invoke {@link #close()}. + */ + public void closeSelf() { + // Assume no action is required. + } + + /** Fully close all loaded alternates and clear the alternate list. */ + public final void closeAlternates() { + ObjectDatabase[] alt = alternates.get(); + if (alt != null) { + alternates.set(null); + closeAlternates(alt); + } + } + + /** + * Does the requested object exist in this database? + * <p> + * Alternates (if present) are searched automatically. + * + * @param objectId + * identity of the object to test for existence of. + * @return true if the specified object is stored in this database, or any + * of the alternate databases. + */ + public final boolean hasObject(final AnyObjectId objectId) { + return hasObjectImpl1(objectId) || hasObjectImpl2(objectId.name()); + } + + private final boolean hasObjectImpl1(final AnyObjectId objectId) { + if (hasObject1(objectId)) { + return true; + } + for (final ObjectDatabase alt : getAlternates()) { + if (alt.hasObjectImpl1(objectId)) { + return true; + } + } + return tryAgain1() && hasObject1(objectId); + } + + private final boolean hasObjectImpl2(final String objectId) { + if (hasObject2(objectId)) { + return true; + } + for (final ObjectDatabase alt : getAlternates()) { + if (alt.hasObjectImpl2(objectId)) { + return true; + } + } + return false; + } + + /** + * Fast half of {@link #hasObject(AnyObjectId)}. + * + * @param objectId + * identity of the object to test for existence of. + * @return true if the specified object is stored in this database. + */ + protected abstract boolean hasObject1(AnyObjectId objectId); + + /** + * Slow half of {@link #hasObject(AnyObjectId)}. + * + * @param objectName + * identity of the object to test for existence of. + * @return true if the specified object is stored in this database. + */ + protected boolean hasObject2(String objectName) { + // Assume the search took place during hasObject1. + return false; + } + + /** + * Open an object from this database. + * <p> + * Alternates (if present) are searched automatically. + * + * @param curs + * temporary working space associated with the calling thread. + * @param objectId + * identity of the object to open. + * @return a {@link ObjectLoader} for accessing the data of the named + * object, or null if the object does not exist. + * @throws IOException + */ + public final ObjectLoader openObject(final WindowCursor curs, + final AnyObjectId objectId) throws IOException { + ObjectLoader ldr; + + ldr = openObjectImpl1(curs, objectId); + if (ldr != null) { + return ldr; + } + + ldr = openObjectImpl2(curs, objectId.name(), objectId); + if (ldr != null) { + return ldr; + } + return null; + } + + private ObjectLoader openObjectImpl1(final WindowCursor curs, + final AnyObjectId objectId) throws IOException { + ObjectLoader ldr; + + ldr = openObject1(curs, objectId); + if (ldr != null) { + return ldr; + } + for (final ObjectDatabase alt : getAlternates()) { + ldr = alt.openObjectImpl1(curs, objectId); + if (ldr != null) { + return ldr; + } + } + if (tryAgain1()) { + ldr = openObject1(curs, objectId); + if (ldr != null) { + return ldr; + } + } + return null; + } + + private ObjectLoader openObjectImpl2(final WindowCursor curs, + final String objectName, final AnyObjectId objectId) + throws IOException { + ObjectLoader ldr; + + ldr = openObject2(curs, objectName, objectId); + if (ldr != null) { + return ldr; + } + for (final ObjectDatabase alt : getAlternates()) { + ldr = alt.openObjectImpl2(curs, objectName, objectId); + if (ldr != null) { + return ldr; + } + } + return null; + } + + /** + * Fast half of {@link #openObject(WindowCursor, AnyObjectId)}. + * + * @param curs + * temporary working space associated with the calling thread. + * @param objectId + * identity of the object to open. + * @return a {@link ObjectLoader} for accessing the data of the named + * object, or null if the object does not exist. + * @throws IOException + */ + protected abstract ObjectLoader openObject1(WindowCursor curs, + AnyObjectId objectId) throws IOException; + + /** + * Slow half of {@link #openObject(WindowCursor, AnyObjectId)}. + * + * @param curs + * temporary working space associated with the calling thread. + * @param objectName + * name of the object to open. + * @param objectId + * identity of the object to open. + * @return a {@link ObjectLoader} for accessing the data of the named + * object, or null if the object does not exist. + * @throws IOException + */ + protected ObjectLoader openObject2(WindowCursor curs, String objectName, + AnyObjectId objectId) throws IOException { + // Assume the search took place during openObject1. + return null; + } + + /** + * Open the object from all packs containing it. + * <p> + * If any alternates are present, their packs are also considered. + * + * @param out + * result collection of loaders for this object, filled with + * loaders from all packs containing specified object + * @param curs + * temporary working space associated with the calling thread. + * @param objectId + * id of object to search for + * @throws IOException + */ + final void openObjectInAllPacks(final Collection<PackedObjectLoader> out, + final WindowCursor curs, final AnyObjectId objectId) + throws IOException { + openObjectInAllPacks1(out, curs, objectId); + for (final ObjectDatabase alt : getAlternates()) { + alt.openObjectInAllPacks1(out, curs, objectId); + } + } + + /** + * Open the object from all packs containing it. + * + * @param out + * result collection of loaders for this object, filled with + * loaders from all packs containing specified object + * @param curs + * temporary working space associated with the calling thread. + * @param objectId + * id of object to search for + * @throws IOException + */ + void openObjectInAllPacks1(Collection<PackedObjectLoader> out, + WindowCursor curs, AnyObjectId objectId) throws IOException { + // Assume no pack support + } + + /** + * @return true if the fast-half search should be tried again. + */ + protected boolean tryAgain1() { + return false; + } + + /** + * Get the alternate databases known to this database. + * + * @return the alternate list. Never null, but may be an empty array. + */ + public final ObjectDatabase[] getAlternates() { + ObjectDatabase[] r = alternates.get(); + if (r == null) { + synchronized (alternates) { + r = alternates.get(); + if (r == null) { + try { + r = loadAlternates(); + } catch (IOException e) { + r = NO_ALTERNATES; + } + alternates.set(r); + } + } + } + return r; + } + + /** + * Load the list of alternate databases into memory. + * <p> + * This method is invoked by {@link #getAlternates()} if the alternate list + * has not yet been populated, or if {@link #closeAlternates()} has been + * called on this instance and the alternate list is needed again. + * <p> + * If the alternate array is empty, implementors should consider using the + * constant {@link #NO_ALTERNATES}. + * + * @return the alternate list for this database. + * @throws IOException + * the alternate list could not be accessed. The empty alternate + * array {@link #NO_ALTERNATES} will be assumed by the caller. + */ + protected ObjectDatabase[] loadAlternates() throws IOException { + return NO_ALTERNATES; + } + + /** + * Close the list of alternates returned by {@link #loadAlternates()}. + * + * @param alt + * the alternate list, from {@link #loadAlternates()}. + */ + protected void closeAlternates(ObjectDatabase[] alt) { + for (final ObjectDatabase d : alt) { + d.close(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDirectory.java new file mode 100644 index 0000000000..297d85f83e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDirectory.java @@ -0,0 +1,517 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jgit.errors.PackMismatchException; +import org.eclipse.jgit.lib.RepositoryCache.FileKey; +import org.eclipse.jgit.util.FS; + +/** + * Traditional file system based {@link ObjectDatabase}. + * <p> + * This is the classical object database representation for a Git repository, + * where objects are stored loose by hashing them into directories by their + * {@link ObjectId}, or are stored in compressed containers known as + * {@link PackFile}s. + */ +public class ObjectDirectory extends ObjectDatabase { + private static final PackList NO_PACKS = new PackList(-1, -1, new PackFile[0]); + + private final File objects; + + private final File infoDirectory; + + private final File packDirectory; + + private final File alternatesFile; + + private final AtomicReference<PackList> packList; + + /** + * Initialize a reference to an on-disk object directory. + * + * @param dir + * the location of the <code>objects</code> directory. + */ + public ObjectDirectory(final File dir) { + objects = dir; + infoDirectory = new File(objects, "info"); + packDirectory = new File(objects, "pack"); + alternatesFile = new File(infoDirectory, "alternates"); + packList = new AtomicReference<PackList>(NO_PACKS); + } + + /** + * @return the location of the <code>objects</code> directory. + */ + public final File getDirectory() { + return objects; + } + + @Override + public boolean exists() { + return objects.exists(); + } + + @Override + public void create() throws IOException { + objects.mkdirs(); + infoDirectory.mkdir(); + packDirectory.mkdir(); + } + + @Override + public void closeSelf() { + final PackList packs = packList.get(); + packList.set(NO_PACKS); + for (final PackFile p : packs.packs) + p.close(); + } + + /** + * Compute the location of a loose object file. + * + * @param objectId + * identity of the loose object to map to the directory. + * @return location of the object, if it were to exist as a loose object. + */ + public File fileFor(final AnyObjectId objectId) { + return fileFor(objectId.name()); + } + + private File fileFor(final String objectName) { + final String d = objectName.substring(0, 2); + final String f = objectName.substring(2); + return new File(new File(objects, d), f); + } + + /** + * Add a single existing pack to the list of available pack files. + * + * @param pack + * path of the pack file to open. + * @param idx + * path of the corresponding index file. + * @throws IOException + * index file could not be opened, read, or is not recognized as + * a Git pack file index. + */ + public void openPack(final File pack, final File idx) throws IOException { + final String p = pack.getName(); + final String i = idx.getName(); + + if (p.length() != 50 || !p.startsWith("pack-") || !p.endsWith(".pack")) + throw new IOException("Not a valid pack " + pack); + + if (i.length() != 49 || !i.startsWith("pack-") || !i.endsWith(".idx")) + throw new IOException("Not a valid pack " + idx); + + if (!p.substring(0, 45).equals(i.substring(0, 45))) + throw new IOException("Pack " + pack + "does not match index"); + + insertPack(new PackFile(idx, pack)); + } + + @Override + public String toString() { + return "ObjectDirectory[" + getDirectory() + "]"; + } + + @Override + protected boolean hasObject1(final AnyObjectId objectId) { + for (final PackFile p : packList.get().packs) { + try { + if (p.hasObject(objectId)) { + return true; + } + } catch (IOException e) { + // The hasObject call should have only touched the index, + // so any failure here indicates the index is unreadable + // by this process, and the pack is likewise not readable. + // + removePack(p); + continue; + } + } + return false; + } + + @Override + protected ObjectLoader openObject1(final WindowCursor curs, + final AnyObjectId objectId) throws IOException { + PackList pList = packList.get(); + SEARCH: for (;;) { + for (final PackFile p : pList.packs) { + try { + final PackedObjectLoader ldr = p.get(curs, objectId); + if (ldr != null) { + ldr.materialize(curs); + return ldr; + } + } catch (PackMismatchException e) { + // Pack was modified; refresh the entire pack list. + // + pList = scanPacks(pList); + continue SEARCH; + } catch (IOException e) { + // Assume the pack is corrupted. + // + removePack(p); + } + } + return null; + } + } + + @Override + void openObjectInAllPacks1(final Collection<PackedObjectLoader> out, + final WindowCursor curs, final AnyObjectId objectId) + throws IOException { + PackList pList = packList.get(); + SEARCH: for (;;) { + for (final PackFile p : pList.packs) { + try { + final PackedObjectLoader ldr = p.get(curs, objectId); + if (ldr != null) { + out.add(ldr); + } + } catch (PackMismatchException e) { + // Pack was modified; refresh the entire pack list. + // + pList = scanPacks(pList); + continue SEARCH; + } catch (IOException e) { + // Assume the pack is corrupted. + // + removePack(p); + } + } + break SEARCH; + } + } + + @Override + protected boolean hasObject2(final String objectName) { + return fileFor(objectName).exists(); + } + + @Override + protected ObjectLoader openObject2(final WindowCursor curs, + final String objectName, final AnyObjectId objectId) + throws IOException { + try { + return new UnpackedObjectLoader(fileFor(objectName), objectId); + } catch (FileNotFoundException noFile) { + return null; + } + } + + @Override + protected boolean tryAgain1() { + final PackList old = packList.get(); + if (old.tryAgain(packDirectory.lastModified())) + return old != scanPacks(old); + return false; + } + + private void insertPack(final PackFile pf) { + PackList o, n; + do { + o = packList.get(); + final PackFile[] oldList = o.packs; + final PackFile[] newList = new PackFile[1 + oldList.length]; + newList[0] = pf; + System.arraycopy(oldList, 0, newList, 1, oldList.length); + n = new PackList(o.lastRead, o.lastModified, newList); + } while (!packList.compareAndSet(o, n)); + } + + private void removePack(final PackFile deadPack) { + PackList o, n; + do { + o = packList.get(); + + final PackFile[] oldList = o.packs; + final int j = indexOf(oldList, deadPack); + if (j < 0) + break; + + final PackFile[] newList = new PackFile[oldList.length - 1]; + System.arraycopy(oldList, 0, newList, 0, j); + System.arraycopy(oldList, j + 1, newList, j, newList.length - j); + n = new PackList(o.lastRead, o.lastModified, newList); + } while (!packList.compareAndSet(o, n)); + deadPack.close(); + } + + private static int indexOf(final PackFile[] list, final PackFile pack) { + for (int i = 0; i < list.length; i++) { + if (list[i] == pack) + return i; + } + return -1; + } + + private PackList scanPacks(final PackList original) { + synchronized (packList) { + PackList o, n; + do { + o = packList.get(); + if (o != original) { + // Another thread did the scan for us, while we + // were blocked on the monitor above. + // + return o; + } + n = scanPacksImpl(o); + if (n == o) + return n; + } while (!packList.compareAndSet(o, n)); + return n; + } + } + + private PackList scanPacksImpl(final PackList old) { + final Map<String, PackFile> forReuse = reuseMap(old); + final long lastRead = System.currentTimeMillis(); + final long lastModified = packDirectory.lastModified(); + final Set<String> names = listPackDirectory(); + final List<PackFile> list = new ArrayList<PackFile>(names.size() >> 2); + boolean foundNew = false; + for (final String indexName : names) { + // Must match "pack-[0-9a-f]{40}.idx" to be an index. + // + if (indexName.length() != 49 || !indexName.endsWith(".idx")) + continue; + + final String base = indexName.substring(0, indexName.length() - 4); + final String packName = base + ".pack"; + if (!names.contains(packName)) { + // Sometimes C Git's HTTP fetch transport leaves a + // .idx file behind and does not download the .pack. + // We have to skip over such useless indexes. + // + continue; + } + + final PackFile oldPack = forReuse.remove(packName); + if (oldPack != null) { + list.add(oldPack); + continue; + } + + final File packFile = new File(packDirectory, packName); + final File idxFile = new File(packDirectory, indexName); + list.add(new PackFile(idxFile, packFile)); + foundNew = true; + } + + // If we did not discover any new files, the modification time was not + // changed, and we did not remove any files, then the set of files is + // the same as the set we were given. Instead of building a new object + // return the same collection. + // + if (!foundNew && lastModified == old.lastModified && forReuse.isEmpty()) + return old.updateLastRead(lastRead); + + for (final PackFile p : forReuse.values()) { + p.close(); + } + + if (list.isEmpty()) + return new PackList(lastRead, lastModified, NO_PACKS.packs); + + final PackFile[] r = list.toArray(new PackFile[list.size()]); + Arrays.sort(r, PackFile.SORT); + return new PackList(lastRead, lastModified, r); + } + + private static Map<String, PackFile> reuseMap(final PackList old) { + final Map<String, PackFile> forReuse = new HashMap<String, PackFile>(); + for (final PackFile p : old.packs) { + if (p.invalid()) { + // The pack instance is corrupted, and cannot be safely used + // again. Do not include it in our reuse map. + // + p.close(); + continue; + } + + final PackFile prior = forReuse.put(p.getPackFile().getName(), p); + if (prior != null) { + // This should never occur. It should be impossible for us + // to have two pack files with the same name, as all of them + // came out of the same directory. If it does, we promised to + // close any PackFiles we did not reuse, so close the one we + // just evicted out of the reuse map. + // + prior.close(); + } + } + return forReuse; + } + + private Set<String> listPackDirectory() { + final String[] nameList = packDirectory.list(); + if (nameList == null) + return Collections.emptySet(); + final Set<String> nameSet = new HashSet<String>(nameList.length << 1); + for (final String name : nameList) { + if (name.startsWith("pack-")) + nameSet.add(name); + } + return nameSet; + } + + @Override + protected ObjectDatabase[] loadAlternates() throws IOException { + final BufferedReader br = open(alternatesFile); + final List<ObjectDatabase> l = new ArrayList<ObjectDatabase>(4); + try { + String line; + while ((line = br.readLine()) != null) { + l.add(openAlternate(line)); + } + } finally { + br.close(); + } + + if (l.isEmpty()) { + return NO_ALTERNATES; + } + return l.toArray(new ObjectDatabase[l.size()]); + } + + private static BufferedReader open(final File f) + throws FileNotFoundException { + return new BufferedReader(new FileReader(f)); + } + + private ObjectDatabase openAlternate(final String location) + throws IOException { + final File objdir = FS.resolve(objects, location); + final File parent = objdir.getParentFile(); + if (FileKey.isGitRepository(parent)) { + final Repository db = RepositoryCache.open(FileKey.exact(parent)); + return new AlternateRepositoryDatabase(db); + } + return new ObjectDirectory(objdir); + } + + private static final class PackList { + /** Last wall-clock time the directory was read. */ + volatile long lastRead; + + /** Last modification time of {@link ObjectDirectory#packDirectory}. */ + final long lastModified; + + /** All known packs, sorted by {@link PackFile#SORT}. */ + final PackFile[] packs; + + private boolean cannotBeRacilyClean; + + PackList(final long lastRead, final long lastModified, + final PackFile[] packs) { + this.lastRead = lastRead; + this.lastModified = lastModified; + this.packs = packs; + this.cannotBeRacilyClean = notRacyClean(lastRead); + } + + private boolean notRacyClean(final long read) { + return read - lastModified > 2 * 60 * 1000L; + } + + PackList updateLastRead(final long now) { + if (notRacyClean(now)) + cannotBeRacilyClean = true; + lastRead = now; + return this; + } + + boolean tryAgain(final long currLastModified) { + // Any difference indicates the directory was modified. + // + if (lastModified != currLastModified) + return true; + + // We have already determined the last read was far enough + // after the last modification that any new modifications + // are certain to change the last modified time. + // + if (cannotBeRacilyClean) + return false; + + if (notRacyClean(lastRead)) { + // Our last read should have marked cannotBeRacilyClean, + // but this thread may not have seen the change. The read + // of the volatile field lastRead should have fixed that. + // + return false; + } + + // We last read this directory too close to its last observed + // modification time. We may have missed a modification. Scan + // the directory again, to ensure we still see the same state. + // + return true; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java new file mode 100644 index 0000000000..2e3506619f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import org.eclipse.jgit.errors.InvalidObjectIdException; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * A SHA-1 abstraction. + */ +public class ObjectId extends AnyObjectId { + private static final ObjectId ZEROID; + + private static final String ZEROID_STR; + + static { + ZEROID = new ObjectId(0, 0, 0, 0, 0); + ZEROID_STR = ZEROID.name(); + } + + /** + * Get the special all-null ObjectId. + * + * @return the all-null ObjectId, often used to stand-in for no object. + */ + public static final ObjectId zeroId() { + return ZEROID; + } + + /** + * Test a string of characters to verify it is a hex format. + * <p> + * If true the string can be parsed with {@link #fromString(String)}. + * + * @param id + * the string to test. + * @return true if the string can converted into an ObjectId. + */ + public static final boolean isId(final String id) { + if (id.length() != STR_LEN) + return false; + try { + for (int i = 0; i < STR_LEN; i++) { + RawParseUtils.parseHexInt4((byte) id.charAt(i)); + } + return true; + } catch (ArrayIndexOutOfBoundsException e) { + return false; + } + } + + /** + * Convert an ObjectId into a hex string representation. + * + * @param i + * the id to convert. May be null. + * @return the hex string conversion of this id's content. + */ + public static final String toString(final ObjectId i) { + return i != null ? i.name() : ZEROID_STR; + } + + /** + * Compare to object identifier byte sequences for equality. + * + * @param firstBuffer + * the first buffer to compare against. Must have at least 20 + * bytes from position ai through the end of the buffer. + * @param fi + * first offset within firstBuffer to begin testing. + * @param secondBuffer + * the second buffer to compare against. Must have at least 2 + * bytes from position bi through the end of the buffer. + * @param si + * first offset within secondBuffer to begin testing. + * @return true if the two identifiers are the same. + */ + public static boolean equals(final byte[] firstBuffer, final int fi, + final byte[] secondBuffer, final int si) { + return firstBuffer[fi] == secondBuffer[si] + && firstBuffer[fi + 1] == secondBuffer[si + 1] + && firstBuffer[fi + 2] == secondBuffer[si + 2] + && firstBuffer[fi + 3] == secondBuffer[si + 3] + && firstBuffer[fi + 4] == secondBuffer[si + 4] + && firstBuffer[fi + 5] == secondBuffer[si + 5] + && firstBuffer[fi + 6] == secondBuffer[si + 6] + && firstBuffer[fi + 7] == secondBuffer[si + 7] + && firstBuffer[fi + 8] == secondBuffer[si + 8] + && firstBuffer[fi + 9] == secondBuffer[si + 9] + && firstBuffer[fi + 10] == secondBuffer[si + 10] + && firstBuffer[fi + 11] == secondBuffer[si + 11] + && firstBuffer[fi + 12] == secondBuffer[si + 12] + && firstBuffer[fi + 13] == secondBuffer[si + 13] + && firstBuffer[fi + 14] == secondBuffer[si + 14] + && firstBuffer[fi + 15] == secondBuffer[si + 15] + && firstBuffer[fi + 16] == secondBuffer[si + 16] + && firstBuffer[fi + 17] == secondBuffer[si + 17] + && firstBuffer[fi + 18] == secondBuffer[si + 18] + && firstBuffer[fi + 19] == secondBuffer[si + 19]; + } + + /** + * Convert an ObjectId from raw binary representation. + * + * @param bs + * the raw byte buffer to read from. At least 20 bytes must be + * available within this byte array. + * @return the converted object id. + */ + public static final ObjectId fromRaw(final byte[] bs) { + return fromRaw(bs, 0); + } + + /** + * Convert an ObjectId from raw binary representation. + * + * @param bs + * the raw byte buffer to read from. At least 20 bytes after p + * must be available within this byte array. + * @param p + * position to read the first byte of data from. + * @return the converted object id. + */ + public static final ObjectId fromRaw(final byte[] bs, final int p) { + final int a = NB.decodeInt32(bs, p); + final int b = NB.decodeInt32(bs, p + 4); + final int c = NB.decodeInt32(bs, p + 8); + final int d = NB.decodeInt32(bs, p + 12); + final int e = NB.decodeInt32(bs, p + 16); + return new ObjectId(a, b, c, d, e); + } + + /** + * Convert an ObjectId from raw binary representation. + * + * @param is + * the raw integers buffer to read from. At least 5 integers must + * be available within this int array. + * @return the converted object id. + */ + public static final ObjectId fromRaw(final int[] is) { + return fromRaw(is, 0); + } + + /** + * Convert an ObjectId from raw binary representation. + * + * @param is + * the raw integers buffer to read from. At least 5 integers + * after p must be available within this int array. + * @param p + * position to read the first integer of data from. + * @return the converted object id. + */ + public static final ObjectId fromRaw(final int[] is, final int p) { + return new ObjectId(is[p], is[p + 1], is[p + 2], is[p + 3], is[p + 4]); + } + + /** + * Convert an ObjectId from hex characters (US-ASCII). + * + * @param buf + * the US-ASCII buffer to read from. At least 40 bytes after + * offset must be available within this byte array. + * @param offset + * position to read the first character from. + * @return the converted object id. + */ + public static final ObjectId fromString(final byte[] buf, final int offset) { + return fromHexString(buf, offset); + } + + /** + * Convert an ObjectId from hex characters. + * + * @param str + * the string to read from. Must be 40 characters long. + * @return the converted object id. + */ + public static final ObjectId fromString(final String str) { + if (str.length() != STR_LEN) + throw new IllegalArgumentException("Invalid id: " + str); + return fromHexString(Constants.encodeASCII(str), 0); + } + + private static final ObjectId fromHexString(final byte[] bs, int p) { + try { + final int a = RawParseUtils.parseHexInt32(bs, p); + final int b = RawParseUtils.parseHexInt32(bs, p + 8); + final int c = RawParseUtils.parseHexInt32(bs, p + 16); + final int d = RawParseUtils.parseHexInt32(bs, p + 24); + final int e = RawParseUtils.parseHexInt32(bs, p + 32); + return new ObjectId(a, b, c, d, e); + } catch (ArrayIndexOutOfBoundsException e1) { + throw new InvalidObjectIdException(bs, p, STR_LEN); + } + } + + ObjectId(final int new_1, final int new_2, final int new_3, + final int new_4, final int new_5) { + w1 = new_1; + w2 = new_2; + w3 = new_3; + w4 = new_4; + w5 = new_5; + } + + /** + * Initialize this instance by copying another existing ObjectId. + * <p> + * This constructor is mostly useful for subclasses who want to extend an + * ObjectId with more properties, but initialize from an existing ObjectId + * instance acquired by other means. + * + * @param src + * another already parsed ObjectId to copy the value out of. + */ + protected ObjectId(final AnyObjectId src) { + w1 = src.w1; + w2 = src.w2; + w3 = src.w3; + w4 = src.w4; + w5 = src.w5; + } + + @Override + public ObjectId toObjectId() { + return this; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java new file mode 100644 index 0000000000..bc4072f607 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.util.Iterator; + +/** + * Fast, efficient map specifically for {@link ObjectId} subclasses. + * <p> + * This map provides an efficient translation from any ObjectId instance to a + * cached subclass of ObjectId that has the same value. + * <p> + * Raw value equality is tested when comparing two ObjectIds (or subclasses), + * not reference equality and not <code>.equals(Object)</code> equality. This + * allows subclasses to override <code>equals</code> to supply their own + * extended semantics. + * + * @param <V> + * type of subclass of ObjectId that will be stored in the map. + */ +public class ObjectIdSubclassMap<V extends ObjectId> implements Iterable<V> { + private int size; + + private V[] obj_hash; + + /** Create an empty map. */ + public ObjectIdSubclassMap() { + obj_hash = createArray(32); + } + + /** Remove all entries from this map. */ + public void clear() { + size = 0; + obj_hash = createArray(32); + } + + /** + * Lookup an existing mapping. + * + * @param toFind + * the object identifier to find. + * @return the instance mapped to toFind, or null if no mapping exists. + */ + public V get(final AnyObjectId toFind) { + int i = index(toFind); + V obj; + + while ((obj = obj_hash[i]) != null) { + if (AnyObjectId.equals(obj, toFind)) + return obj; + if (++i == obj_hash.length) + i = 0; + } + return null; + } + + /** + * Store an object for future lookup. + * <p> + * An existing mapping for <b>must not</b> be in this map. Callers must + * first call {@link #get(AnyObjectId)} to verify there is no current + * mapping prior to adding a new mapping. + * + * @param newValue + * the object to store. + * @param + * <Q> + * type of instance to store. + */ + public <Q extends V> void add(final Q newValue) { + if (obj_hash.length - 1 <= size * 2) + grow(); + insert(newValue); + size++; + } + + /** + * @return number of objects in map + */ + public int size() { + return size; + } + + public Iterator<V> iterator() { + return new Iterator<V>() { + private int found; + + private int i; + + public boolean hasNext() { + return found < size; + } + + public V next() { + while (i < obj_hash.length) { + final V v = obj_hash[i++]; + if (v != null) { + found++; + return v; + } + } + throw new IllegalStateException(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + private final int index(final AnyObjectId id) { + return (id.w1 >>> 1) % obj_hash.length; + } + + private void insert(final V newValue) { + int j = index(newValue); + while (obj_hash[j] != null) { + if (++j >= obj_hash.length) + j = 0; + } + obj_hash[j] = newValue; + } + + private void grow() { + final V[] old_hash = obj_hash; + final int old_hash_size = obj_hash.length; + + obj_hash = createArray(2 * old_hash_size); + for (int i = 0; i < old_hash_size; i++) { + final V obj = old_hash[i]; + if (obj != null) + insert(obj); + } + } + + @SuppressWarnings("unchecked") + private final V[] createArray(final int sz) { + return (V[]) new ObjectId[sz]; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectLoader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectLoader.java new file mode 100644 index 0000000000..97b3b769aa --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectLoader.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Jonas Fonseca <fonseca@diku.dk> + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + + +/** + * Base class for a set of loaders for different representations of Git objects. + * New loaders are constructed for every object. + */ +public abstract class ObjectLoader { + /** + * @return Git in pack object type, see {@link Constants}. + */ + public abstract int getType(); + + /** + * @return size of object in bytes + */ + public abstract long getSize(); + + /** + * Obtain a copy of the bytes of this object. + * <p> + * Unlike {@link #getCachedBytes()} this method returns an array that might + * be modified by the caller. + * + * @return the bytes of this object. + */ + public final byte[] getBytes() { + final byte[] data = getCachedBytes(); + final byte[] copy = new byte[data.length]; + System.arraycopy(data, 0, copy, 0, data.length); + return copy; + } + + /** + * Obtain a reference to the (possibly cached) bytes of this object. + * <p> + * This method offers direct access to the internal caches, potentially + * saving on data copies between the internal cache and higher level code. + * Callers who receive this reference <b>must not</b> modify its contents. + * Changes (if made) will affect the cache but not the repository itself. + * + * @return the cached bytes of this object. Do not modify it. + */ + public abstract byte[] getCachedBytes(); + + /** + * @return raw object type from object header, as stored in storage (pack, + * loose file). This may be different from {@link #getType()} result + * for packs (see {@link Constants}). + */ + public abstract int getRawType(); + + /** + * @return raw size of object from object header (pack, loose file). + * Interpretation of this value depends on {@link #getRawType()}. + */ + public abstract long getRawSize(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectWriter.java new file mode 100644 index 0000000000..60e85eb57f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectWriter.java @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.security.MessageDigest; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +import org.eclipse.jgit.errors.ObjectWritingException; + +/** + * A class for writing loose objects. + */ +public class ObjectWriter { + private static final byte[] htree = Constants.encodeASCII("tree"); + + private static final byte[] hparent = Constants.encodeASCII("parent"); + + private static final byte[] hauthor = Constants.encodeASCII("author"); + + private static final byte[] hcommitter = Constants.encodeASCII("committer"); + + private static final byte[] hencoding = Constants.encodeASCII("encoding"); + + private final Repository r; + + private final byte[] buf; + + private final MessageDigest md; + + private final Deflater def; + + /** + * Construct an Object writer for the specified repository + * @param d + */ + public ObjectWriter(final Repository d) { + r = d; + buf = new byte[8192]; + md = Constants.newMessageDigest(); + def = new Deflater(r.getConfig().getCore().getCompression()); + } + + /** + * Write a blob with the specified data + * + * @param b bytes of the blob + * @return SHA-1 of the blob + * @throws IOException + */ + public ObjectId writeBlob(final byte[] b) throws IOException { + return writeBlob(b.length, new ByteArrayInputStream(b)); + } + + /** + * Write a blob with the data in the specified file + * + * @param f + * a file containing blob data + * @return SHA-1 of the blob + * @throws IOException + */ + public ObjectId writeBlob(final File f) throws IOException { + final FileInputStream is = new FileInputStream(f); + try { + return writeBlob(f.length(), is); + } finally { + is.close(); + } + } + + /** + * Write a blob with data from a stream + * + * @param len + * number of bytes to consume from the stream + * @param is + * stream with blob data + * @return SHA-1 of the blob + * @throws IOException + */ + public ObjectId writeBlob(final long len, final InputStream is) + throws IOException { + return writeObject(Constants.OBJ_BLOB, len, is, true); + } + + /** + * Write a Tree to the object database. + * + * @param t + * Tree + * @return SHA-1 of the tree + * @throws IOException + */ + public ObjectId writeTree(final Tree t) throws IOException { + final ByteArrayOutputStream o = new ByteArrayOutputStream(); + final TreeEntry[] items = t.members(); + for (int k = 0; k < items.length; k++) { + final TreeEntry e = items[k]; + final ObjectId id = e.getId(); + + if (id == null) + throw new ObjectWritingException("Object at path \"" + + e.getFullName() + "\" does not have an id assigned." + + " All object ids must be assigned prior" + + " to writing a tree."); + + e.getMode().copyTo(o); + o.write(' '); + o.write(e.getNameUTF8()); + o.write(0); + id.copyRawTo(o); + } + return writeCanonicalTree(o.toByteArray()); + } + + /** + * Write a canonical tree to the object database. + * + * @param b + * the canonical encoding of the tree object. + * @return SHA-1 of the tree + * @throws IOException + */ + public ObjectId writeCanonicalTree(final byte[] b) throws IOException { + return writeTree(b.length, new ByteArrayInputStream(b)); + } + + private ObjectId writeTree(final long len, final InputStream is) + throws IOException { + return writeObject(Constants.OBJ_TREE, len, is, true); + } + + /** + * Write a Commit to the object database + * + * @param c + * Commit to store + * @return SHA-1 of the commit + * @throws IOException + */ + public ObjectId writeCommit(final Commit c) throws IOException { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + String encoding = c.getEncoding(); + if (encoding == null) + encoding = Constants.CHARACTER_ENCODING; + final OutputStreamWriter w = new OutputStreamWriter(os, encoding); + + os.write(htree); + os.write(' '); + c.getTreeId().copyTo(os); + os.write('\n'); + + ObjectId[] ps = c.getParentIds(); + for (int i=0; i<ps.length; ++i) { + os.write(hparent); + os.write(' '); + ps[i].copyTo(os); + os.write('\n'); + } + + os.write(hauthor); + os.write(' '); + w.write(c.getAuthor().toExternalString()); + w.flush(); + os.write('\n'); + + os.write(hcommitter); + os.write(' '); + w.write(c.getCommitter().toExternalString()); + w.flush(); + os.write('\n'); + + if (!encoding.equals(Constants.CHARACTER_ENCODING)) { + os.write(hencoding); + os.write(' '); + os.write(Constants.encodeASCII(encoding)); + os.write('\n'); + } + + os.write('\n'); + w.write(c.getMessage()); + w.flush(); + + return writeCommit(os.toByteArray()); + } + + private ObjectId writeTag(final byte[] b) throws IOException { + return writeTag(b.length, new ByteArrayInputStream(b)); + } + + /** + * Write an annotated Tag to the object database + * + * @param c + * Tag + * @return SHA-1 of the tag + * @throws IOException + */ + public ObjectId writeTag(final Tag c) throws IOException { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + final OutputStreamWriter w = new OutputStreamWriter(os, + Constants.CHARSET); + + w.write("object "); + c.getObjId().copyTo(w); + w.write('\n'); + + w.write("type "); + w.write(c.getType()); + w.write("\n"); + + w.write("tag "); + w.write(c.getTag()); + w.write("\n"); + + w.write("tagger "); + w.write(c.getAuthor().toExternalString()); + w.write('\n'); + + w.write('\n'); + w.write(c.getMessage()); + w.close(); + + return writeTag(os.toByteArray()); + } + + private ObjectId writeCommit(final byte[] b) throws IOException { + return writeCommit(b.length, new ByteArrayInputStream(b)); + } + + private ObjectId writeCommit(final long len, final InputStream is) + throws IOException { + return writeObject(Constants.OBJ_COMMIT, len, is, true); + } + + private ObjectId writeTag(final long len, final InputStream is) + throws IOException { + return writeObject(Constants.OBJ_TAG, len, is, true); + } + + /** + * Compute the SHA-1 of a blob without creating an object. This is for + * figuring out if we already have a blob or not. + * + * @param len number of bytes to consume + * @param is stream for read blob data from + * @return SHA-1 of a looked for blob + * @throws IOException + */ + public ObjectId computeBlobSha1(final long len, final InputStream is) + throws IOException { + return writeObject(Constants.OBJ_BLOB, len, is, false); + } + + ObjectId writeObject(final int type, long len, final InputStream is, + boolean store) throws IOException { + final File t; + final DeflaterOutputStream deflateStream; + final FileOutputStream fileStream; + ObjectId id = null; + + if (store) { + t = File.createTempFile("noz", null, r.getObjectsDirectory()); + fileStream = new FileOutputStream(t); + } else { + t = null; + fileStream = null; + } + + md.reset(); + if (store) { + def.reset(); + deflateStream = new DeflaterOutputStream(fileStream, def); + } else + deflateStream = null; + + try { + byte[] header; + int n; + + header = Constants.encodedTypeString(type); + md.update(header); + if (deflateStream != null) + deflateStream.write(header); + + md.update((byte) ' '); + if (deflateStream != null) + deflateStream.write((byte) ' '); + + header = Constants.encodeASCII(len); + md.update(header); + if (deflateStream != null) + deflateStream.write(header); + + md.update((byte) 0); + if (deflateStream != null) + deflateStream.write((byte) 0); + + while (len > 0 + && (n = is.read(buf, 0, (int) Math.min(len, buf.length))) > 0) { + md.update(buf, 0, n); + if (deflateStream != null) + deflateStream.write(buf, 0, n); + len -= n; + } + + if (len != 0) + throw new IOException("Input did not match supplied length. " + + len + " bytes are missing."); + + if (deflateStream != null ) { + deflateStream.close(); + if (t != null) + t.setReadOnly(); + } + + id = ObjectId.fromRaw(md.digest()); + } finally { + if (id == null && deflateStream != null) { + try { + deflateStream.close(); + } finally { + t.delete(); + } + } + } + + if (t == null) + return id; + + if (r.hasObject(id)) { + // Object is already in the repository so remove + // the temporary file. + // + t.delete(); + } else { + final File o = r.toFile(id); + if (!t.renameTo(o)) { + // Maybe the directory doesn't exist yet as the object + // directories are always lazily created. Note that we + // try the rename first as the directory likely does exist. + // + o.getParentFile().mkdir(); + if (!t.renameTo(o)) { + if (!r.hasObject(id)) { + // The object failed to be renamed into its proper + // location and it doesn't exist in the repository + // either. We really don't know what went wrong, so + // fail. + // + t.delete(); + throw new ObjectWritingException("Unable to" + + " create new object: " + o); + } + } + } + } + + return id; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/OffsetCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/OffsetCache.java new file mode 100644 index 0000000000..747f6f122e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/OffsetCache.java @@ -0,0 +1,539 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReferenceArray; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Least frequently used cache for objects specified by PackFile positions. + * <p> + * This cache maps a <code>({@link PackFile},position)</code> tuple to an Object. + * <p> + * This cache is suitable for objects that are "relative expensive" to compute + * from the underlying PackFile, given some known position in that file. + * <p> + * Whenever a cache miss occurs, {@link #load(PackFile, long)} is invoked by + * exactly one thread for the given <code>(PackFile,position)</code> key tuple. + * This is ensured by an array of locks, with the tuple hashed to a lock + * instance. + * <p> + * During a miss, older entries are evicted from the cache so long as + * {@link #isFull()} returns true. + * <p> + * Its too expensive during object access to be 100% accurate with a least + * recently used (LRU) algorithm. Strictly ordering every read is a lot of + * overhead that typically doesn't yield a corresponding benefit to the + * application. + * <p> + * This cache implements a loose LRU policy by randomly picking a window + * comprised of roughly 10% of the cache, and evicting the oldest accessed entry + * within that window. + * <p> + * Entities created by the cache are held under SoftReferences, permitting the + * Java runtime's garbage collector to evict entries when heap memory gets low. + * Most JREs implement a loose least recently used algorithm for this eviction. + * <p> + * The internal hash table does not expand at runtime, instead it is fixed in + * size at cache creation time. The internal lock table used to gate load + * invocations is also fixed in size. + * <p> + * The key tuple is passed through to methods as a pair of parameters rather + * than as a single Object, thus reducing the transient memory allocations of + * callers. It is more efficient to avoid the allocation, as we can't be 100% + * sure that a JIT would be able to stack-allocate a key tuple. + * <p> + * This cache has an implementation rule such that: + * <ul> + * <li>{@link #load(PackFile, long)} is invoked by at most one thread at a time + * for a given <code>(PackFile,position)</code> tuple.</li> + * <li>For every <code>load()</code> invocation there is exactly one + * {@link #createRef(PackFile, long, Object)} invocation to wrap a SoftReference + * around the cached entity.</li> + * <li>For every Reference created by <code>createRef()</code> there will be + * exactly one call to {@link #clear(Ref)} to cleanup any resources associated + * with the (now expired) cached entity.</li> + * </ul> + * <p> + * Therefore, it is safe to perform resource accounting increments during the + * {@link #load(PackFile, long)} or {@link #createRef(PackFile, long, Object)} + * methods, and matching decrements during {@link #clear(Ref)}. Implementors may + * need to override {@link #createRef(PackFile, long, Object)} in order to embed + * additional accounting information into an implementation specific + * {@link OffsetCache.Ref} subclass, as the cached entity may have already been + * evicted by the JRE's garbage collector. + * <p> + * To maintain higher concurrency workloads, during eviction only one thread + * performs the eviction work, while other threads can continue to insert new + * objects in parallel. This means that the cache can be temporarily over limit, + * especially if the nominated eviction thread is being starved relative to the + * other threads. + * + * @param <V> + * type of value stored in the cache. + * @param <R> + * type of {@link OffsetCache.Ref} subclass used by the cache. + */ +abstract class OffsetCache<V, R extends OffsetCache.Ref<V>> { + private static final Random rng = new Random(); + + /** ReferenceQueue that {@link #createRef(PackFile, long, Object)} must use. */ + protected final ReferenceQueue<V> queue; + + /** Number of entries in {@link #table}. */ + private final int tableSize; + + /** Access clock for loose LRU. */ + private final AtomicLong clock; + + /** Hash bucket directory; entries are chained below. */ + private final AtomicReferenceArray<Entry<V>> table; + + /** Locks to prevent concurrent loads for same (PackFile,position). */ + private final Lock[] locks; + + /** Lock to elect the eviction thread after a load occurs. */ + private final ReentrantLock evictLock; + + /** Number of {@link #table} buckets to scan for an eviction window. */ + private final int evictBatch; + + /** + * Create a new cache with a fixed size entry table and lock table. + * + * @param tSize + * number of entries in the entry hash table. + * @param lockCount + * number of entries in the lock table. This is the maximum + * concurrency rate for creation of new objects through + * {@link #load(PackFile, long)} invocations. + */ + OffsetCache(final int tSize, final int lockCount) { + if (tSize < 1) + throw new IllegalArgumentException("tSize must be >= 1"); + if (lockCount < 1) + throw new IllegalArgumentException("lockCount must be >= 1"); + + queue = new ReferenceQueue<V>(); + tableSize = tSize; + clock = new AtomicLong(1); + table = new AtomicReferenceArray<Entry<V>>(tableSize); + locks = new Lock[lockCount]; + for (int i = 0; i < locks.length; i++) + locks[i] = new Lock(); + evictLock = new ReentrantLock(); + + int eb = (int) (tableSize * .1); + if (64 < eb) + eb = 64; + else if (eb < 4) + eb = 4; + if (tableSize < eb) + eb = tableSize; + evictBatch = eb; + } + + /** + * Lookup a cached object, creating and loading it if it doesn't exist. + * + * @param pack + * the pack that "contains" the cached object. + * @param position + * offset within <code>pack</code> of the object. + * @return the object reference. + * @throws IOException + * the object reference was not in the cache and could not be + * obtained by {@link #load(PackFile, long)}. + */ + V getOrLoad(final PackFile pack, final long position) throws IOException { + final int slot = slot(pack, position); + final Entry<V> e1 = table.get(slot); + V v = scan(e1, pack, position); + if (v != null) + return v; + + synchronized (lock(pack, position)) { + Entry<V> e2 = table.get(slot); + if (e2 != e1) { + v = scan(e2, pack, position); + if (v != null) + return v; + } + + v = load(pack, position); + final Ref<V> ref = createRef(pack, position, v); + hit(ref); + for (;;) { + final Entry<V> n = new Entry<V>(clean(e2), ref); + if (table.compareAndSet(slot, e2, n)) + break; + e2 = table.get(slot); + } + } + + if (evictLock.tryLock()) { + try { + gc(); + evict(); + } finally { + evictLock.unlock(); + } + } + + return v; + } + + private V scan(Entry<V> n, final PackFile pack, final long position) { + for (; n != null; n = n.next) { + final Ref<V> r = n.ref; + if (r.pack == pack && r.position == position) { + final V v = r.get(); + if (v != null) { + hit(r); + return v; + } + n.kill(); + break; + } + } + return null; + } + + private void hit(final Ref<V> r) { + // We don't need to be 100% accurate here. Its sufficient that at least + // one thread performs the increment. Any other concurrent access at + // exactly the same time can simply use the same clock value. + // + // Consequently we attempt the set, but we don't try to recover should + // it fail. This is why we don't use getAndIncrement() here. + // + final long c = clock.get(); + clock.compareAndSet(c, c + 1); + r.lastAccess = c; + } + + private void evict() { + while (isFull()) { + int ptr = rng.nextInt(tableSize); + Entry<V> old = null; + int slot = 0; + for (int b = evictBatch - 1; b >= 0; b--, ptr++) { + if (tableSize <= ptr) + ptr = 0; + for (Entry<V> e = table.get(ptr); e != null; e = e.next) { + if (e.dead) + continue; + if (old == null || e.ref.lastAccess < old.ref.lastAccess) { + old = e; + slot = ptr; + } + } + } + if (old != null) { + old.kill(); + gc(); + final Entry<V> e1 = table.get(slot); + table.compareAndSet(slot, e1, clean(e1)); + } + } + } + + /** + * Clear every entry from the cache. + *<p> + * This is a last-ditch effort to clear out the cache, such as before it + * gets replaced by another cache that is configured differently. This + * method tries to force every cached entry through {@link #clear(Ref)} to + * ensure that resources are correctly accounted for and cleaned up by the + * subclass. A concurrent reader loading entries while this method is + * running may cause resource accounting failures. + */ + void removeAll() { + for (int s = 0; s < tableSize; s++) { + Entry<V> e1; + do { + e1 = table.get(s); + for (Entry<V> e = e1; e != null; e = e.next) + e.kill(); + } while (!table.compareAndSet(s, e1, null)); + } + gc(); + } + + /** + * Clear all entries related to a single file. + * <p> + * Typically this method is invoked during {@link PackFile#close()}, when we + * know the pack is never going to be useful to us again (for example, it no + * longer exists on disk). A concurrent reader loading an entry from this + * same pack may cause the pack to become stuck in the cache anyway. + * + * @param pack + * the file to purge all entries of. + */ + void removeAll(final PackFile pack) { + for (int s = 0; s < tableSize; s++) { + final Entry<V> e1 = table.get(s); + boolean hasDead = false; + for (Entry<V> e = e1; e != null; e = e.next) { + if (e.ref.pack == pack) { + e.kill(); + hasDead = true; + } else if (e.dead) + hasDead = true; + } + if (hasDead) + table.compareAndSet(s, e1, clean(e1)); + } + gc(); + } + + /** + * Materialize an object that doesn't yet exist in the cache. + * <p> + * This method is invoked by {@link #getOrLoad(PackFile, long)} when the + * specified entity does not yet exist in the cache. Internal locking + * ensures that at most one thread can call this method for each unique + * <code>(pack,position)</code>, but multiple threads can call this method + * concurrently for different <code>(pack,position)</code> tuples. + * + * @param pack + * the file to materialize the entry from. + * @param position + * offset within the file of the entry. + * @return the materialized object. Must never be null. + * @throws IOException + * the method was unable to materialize the object for this + * input pair. The usual reasons would be file corruption, file + * not found, out of file descriptors, etc. + */ + protected abstract V load(PackFile pack, long position) throws IOException; + + /** + * Construct a Ref (SoftReference) around a cached entity. + * <p> + * Implementing this is only necessary if the subclass is performing + * resource accounting during {@link #load(PackFile, long)} and + * {@link #clear(Ref)} requires some information to update the accounting. + * <p> + * Implementors <b>MUST</b> ensure that the returned reference uses the + * {@link #queue} ReferenceQueue, otherwise {@link #clear(Ref)} will not be + * invoked at the proper time. + * + * @param pack + * the file to materialize the entry from. + * @param position + * offset within the file of the entry. + * @param v + * the object returned by {@link #load(PackFile, long)}. + * @return a soft reference subclass wrapped around <code>v</code>. + */ + @SuppressWarnings("unchecked") + protected R createRef(final PackFile pack, final long position, final V v) { + return (R) new Ref<V>(pack, position, v, queue); + } + + /** + * Update accounting information now that an object has left the cache. + * <p> + * This method is invoked exactly once for the combined + * {@link #load(PackFile, long)} and + * {@link #createRef(PackFile, long, Object)} invocation pair that was used + * to construct and insert an object into the cache. + * + * @param ref + * the reference wrapped around the object. Implementations must + * be prepared for <code>ref.get()</code> to return null. + */ + protected void clear(final R ref) { + // Do nothing by default. + } + + /** + * Determine if the cache is full and requires eviction of entries. + * <p> + * By default this method returns false. Implementors may override to + * consult with the accounting updated by {@link #load(PackFile, long)}, + * {@link #createRef(PackFile, long, Object)} and {@link #clear(Ref)}. + * + * @return true if the cache is still over-limit and requires eviction of + * more entries. + */ + protected boolean isFull() { + return false; + } + + @SuppressWarnings("unchecked") + private void gc() { + R r; + while ((r = (R) queue.poll()) != null) { + // Sun's Java 5 and 6 implementation have a bug where a Reference + // can be enqueued and dequeued twice on the same reference queue + // due to a race condition within ReferenceQueue.enqueue(Reference). + // + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6837858 + // + // We CANNOT permit a Reference to come through us twice, as it will + // skew the resource counters we maintain. Our canClear() check here + // provides a way to skip the redundant dequeues, if any. + // + if (r.canClear()) { + clear(r); + + boolean found = false; + final int s = slot(r.pack, r.position); + final Entry<V> e1 = table.get(s); + for (Entry<V> n = e1; n != null; n = n.next) { + if (n.ref == r) { + n.dead = true; + found = true; + break; + } + } + if (found) + table.compareAndSet(s, e1, clean(e1)); + } + } + } + + /** + * Compute the hash code value for a <code>(PackFile,position)</code> tuple. + * <p> + * For example, <code>return packHash + (int) (position >>> 4)</code>. + * Implementors must override with a suitable hash (for example, a different + * right shift on the position). + * + * @param packHash + * hash code for the file being accessed. + * @param position + * position within the file being accessed. + * @return a reasonable hash code mixing the two values. + */ + protected abstract int hash(int packHash, long position); + + private int slot(final PackFile pack, final long position) { + return (hash(pack.hash, position) >>> 1) % tableSize; + } + + private Lock lock(final PackFile pack, final long position) { + return locks[(hash(pack.hash, position) >>> 1) % locks.length]; + } + + private static <V> Entry<V> clean(Entry<V> top) { + while (top != null && top.dead) { + top.ref.enqueue(); + top = top.next; + } + if (top == null) + return null; + final Entry<V> n = clean(top.next); + return n == top.next ? top : new Entry<V>(n, top.ref); + } + + private static class Entry<V> { + /** Next entry in the hash table's chain list. */ + final Entry<V> next; + + /** The referenced object. */ + final Ref<V> ref; + + /** + * Marked true when ref.get() returns null and the ref is dead. + * <p> + * A true here indicates that the ref is no longer accessible, and that + * we therefore need to eventually purge this Entry object out of the + * bucket's chain. + */ + volatile boolean dead; + + Entry(final Entry<V> n, final Ref<V> r) { + next = n; + ref = r; + } + + final void kill() { + dead = true; + ref.enqueue(); + } + } + + /** + * A soft reference wrapped around a cached object. + * + * @param <V> + * type of the cached object. + */ + protected static class Ref<V> extends SoftReference<V> { + final PackFile pack; + + final long position; + + long lastAccess; + + private boolean cleared; + + protected Ref(final PackFile pack, final long position, final V v, + final ReferenceQueue<V> queue) { + super(v, queue); + this.pack = pack; + this.position = position; + } + + final synchronized boolean canClear() { + if (cleared) + return false; + cleared = true; + return true; + } + } + + private static final class Lock { + // Used only for its implicit monitor. + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackFile.java new file mode 100644 index 0000000000..9defcad918 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackFile.java @@ -0,0 +1,515 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel.MapMode; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.zip.CRC32; +import java.util.zip.CheckedOutputStream; +import java.util.zip.DataFormatException; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.PackInvalidException; +import org.eclipse.jgit.errors.PackMismatchException; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * A Git version 2 pack file representation. A pack file contains Git objects in + * delta packed format yielding high compression of lots of object where some + * objects are similar. + */ +public class PackFile implements Iterable<PackIndex.MutableEntry> { + /** Sorts PackFiles to be most recently created to least recently created. */ + public static Comparator<PackFile> SORT = new Comparator<PackFile>() { + public int compare(final PackFile a, final PackFile b) { + return b.packLastModified - a.packLastModified; + } + }; + + private final File idxFile; + + private final File packFile; + + final int hash; + + private RandomAccessFile fd; + + long length; + + private int activeWindows; + + private int activeCopyRawData; + + private int packLastModified; + + private volatile boolean invalid; + + private byte[] packChecksum; + + private PackIndex loadedIdx; + + private PackReverseIndex reverseIdx; + + /** + * Construct a reader for an existing, pre-indexed packfile. + * + * @param idxFile + * path of the <code>.idx</code> file listing the contents. + * @param packFile + * path of the <code>.pack</code> file holding the data. + */ + public PackFile(final File idxFile, final File packFile) { + this.idxFile = idxFile; + this.packFile = packFile; + this.packLastModified = (int) (packFile.lastModified() >> 10); + + // Multiply by 31 here so we can more directly combine with another + // value in WindowCache.hash(), without doing the multiply there. + // + hash = System.identityHashCode(this) * 31; + length = Long.MAX_VALUE; + } + + private synchronized PackIndex idx() throws IOException { + if (loadedIdx == null) { + if (invalid) + throw new PackInvalidException(packFile); + + try { + final PackIndex idx = PackIndex.open(idxFile); + + if (packChecksum == null) + packChecksum = idx.packChecksum; + else if (!Arrays.equals(packChecksum, idx.packChecksum)) + throw new PackMismatchException("Pack checksum mismatch"); + + loadedIdx = idx; + } catch (IOException e) { + invalid = true; + throw e; + } + } + return loadedIdx; + } + + final PackedObjectLoader resolveBase(final WindowCursor curs, final long ofs) + throws IOException { + return reader(curs, ofs); + } + + /** @return the File object which locates this pack on disk. */ + public File getPackFile() { + return packFile; + } + + /** + * Determine if an object is contained within the pack file. + * <p> + * For performance reasons only the index file is searched; the main pack + * content is ignored entirely. + * </p> + * + * @param id + * the object to look for. Must not be null. + * @return true if the object is in this pack; false otherwise. + * @throws IOException + * the index file cannot be loaded into memory. + */ + public boolean hasObject(final AnyObjectId id) throws IOException { + return idx().hasObject(id); + } + + /** + * Get an object from this pack. + * + * @param curs + * temporary working space associated with the calling thread. + * @param id + * the object to obtain from the pack. Must not be null. + * @return the object loader for the requested object if it is contained in + * this pack; null if the object was not found. + * @throws IOException + * the pack file or the index could not be read. + */ + public PackedObjectLoader get(final WindowCursor curs, final AnyObjectId id) + throws IOException { + final long offset = idx().findOffset(id); + return 0 < offset ? reader(curs, offset) : null; + } + + /** + * Close the resources utilized by this repository + */ + public void close() { + UnpackedObjectCache.purge(this); + WindowCache.purge(this); + synchronized (this) { + loadedIdx = null; + reverseIdx = null; + } + } + + /** + * Provide iterator over entries in associated pack index, that should also + * exist in this pack file. Objects returned by such iterator are mutable + * during iteration. + * <p> + * Iterator returns objects in SHA-1 lexicographical order. + * </p> + * + * @return iterator over entries of associated pack index + * + * @see PackIndex#iterator() + */ + public Iterator<PackIndex.MutableEntry> iterator() { + try { + return idx().iterator(); + } catch (IOException e) { + return Collections.<PackIndex.MutableEntry> emptyList().iterator(); + } + } + + /** + * Obtain the total number of objects available in this pack. This method + * relies on pack index, giving number of effectively available objects. + * + * @return number of objects in index of this pack, likewise in this pack + * @throws IOException + * the index file cannot be loaded into memory. + */ + long getObjectCount() throws IOException { + return idx().getObjectCount(); + } + + /** + * Search for object id with the specified start offset in associated pack + * (reverse) index. + * + * @param offset + * start offset of object to find + * @return object id for this offset, or null if no object was found + * @throws IOException + * the index file cannot be loaded into memory. + */ + ObjectId findObjectForOffset(final long offset) throws IOException { + return getReverseIdx().findObject(offset); + } + + final UnpackedObjectCache.Entry readCache(final long position) { + return UnpackedObjectCache.get(this, position); + } + + final void saveCache(final long position, final byte[] data, final int type) { + UnpackedObjectCache.store(this, position, data, type); + } + + final byte[] decompress(final long position, final int totalSize, + final WindowCursor curs) throws DataFormatException, IOException { + final byte[] dstbuf = new byte[totalSize]; + if (curs.inflate(this, position, dstbuf, 0) != totalSize) + throw new EOFException("Short compressed stream at " + position); + return dstbuf; + } + + final void copyRawData(final PackedObjectLoader loader, + final OutputStream out, final byte buf[], final WindowCursor curs) + throws IOException { + final long objectOffset = loader.objectOffset; + final long dataOffset = loader.dataOffset; + final int cnt = (int) (findEndOffset(objectOffset) - dataOffset); + final PackIndex idx = idx(); + + if (idx.hasCRC32Support()) { + final CRC32 crc = new CRC32(); + int headerCnt = (int) (dataOffset - objectOffset); + while (headerCnt > 0) { + final int toRead = Math.min(headerCnt, buf.length); + readFully(objectOffset, buf, 0, toRead, curs); + crc.update(buf, 0, toRead); + headerCnt -= toRead; + } + final CheckedOutputStream crcOut = new CheckedOutputStream(out, crc); + copyToStream(dataOffset, buf, cnt, crcOut, curs); + final long computed = crc.getValue(); + + final ObjectId id = findObjectForOffset(objectOffset); + final long expected = idx.findCRC32(id); + if (computed != expected) + throw new CorruptObjectException("Object at " + dataOffset + + " in " + getPackFile() + " has bad zlib stream"); + } else { + try { + curs.inflateVerify(this, dataOffset); + } catch (DataFormatException dfe) { + final CorruptObjectException coe; + coe = new CorruptObjectException("Object at " + dataOffset + + " in " + getPackFile() + " has bad zlib stream"); + coe.initCause(dfe); + throw coe; + } + copyToStream(dataOffset, buf, cnt, out, curs); + } + } + + boolean supportsFastCopyRawData() throws IOException { + return idx().hasCRC32Support(); + } + + boolean invalid() { + return invalid; + } + + private void readFully(final long position, final byte[] dstbuf, + int dstoff, final int cnt, final WindowCursor curs) + throws IOException { + if (curs.copy(this, position, dstbuf, dstoff, cnt) != cnt) + throw new EOFException(); + } + + private void copyToStream(long position, final byte[] buf, long cnt, + final OutputStream out, final WindowCursor curs) + throws IOException, EOFException { + while (cnt > 0) { + final int toRead = (int) Math.min(cnt, buf.length); + readFully(position, buf, 0, toRead, curs); + position += toRead; + cnt -= toRead; + out.write(buf, 0, toRead); + } + } + + synchronized void beginCopyRawData() throws IOException { + if (++activeCopyRawData == 1 && activeWindows == 0) + doOpen(); + } + + synchronized void endCopyRawData() { + if (--activeCopyRawData == 0 && activeWindows == 0) + doClose(); + } + + synchronized boolean beginWindowCache() throws IOException { + if (++activeWindows == 1) { + if (activeCopyRawData == 0) + doOpen(); + return true; + } + return false; + } + + synchronized boolean endWindowCache() { + final boolean r = --activeWindows == 0; + if (r && activeCopyRawData == 0) + doClose(); + return r; + } + + private void doOpen() throws IOException { + try { + if (invalid) + throw new PackInvalidException(packFile); + fd = new RandomAccessFile(packFile, "r"); + length = fd.length(); + onOpenPack(); + } catch (IOException ioe) { + openFail(); + throw ioe; + } catch (RuntimeException re) { + openFail(); + throw re; + } catch (Error re) { + openFail(); + throw re; + } + } + + private void openFail() { + activeWindows = 0; + activeCopyRawData = 0; + invalid = true; + doClose(); + } + + private void doClose() { + if (fd != null) { + try { + fd.close(); + } catch (IOException err) { + // Ignore a close event. We had it open only for reading. + // There should not be errors related to network buffers + // not flushed, etc. + } + fd = null; + } + } + + ByteArrayWindow read(final long pos, int size) throws IOException { + if (length < pos + size) + size = (int) (length - pos); + final byte[] buf = new byte[size]; + NB.readFully(fd.getChannel(), pos, buf, 0, size); + return new ByteArrayWindow(this, pos, buf); + } + + ByteWindow mmap(final long pos, int size) throws IOException { + if (length < pos + size) + size = (int) (length - pos); + + MappedByteBuffer map; + try { + map = fd.getChannel().map(MapMode.READ_ONLY, pos, size); + } catch (IOException ioe1) { + // The most likely reason this failed is the JVM has run out + // of virtual memory. We need to discard quickly, and try to + // force the GC to finalize and release any existing mappings. + // + System.gc(); + System.runFinalization(); + map = fd.getChannel().map(MapMode.READ_ONLY, pos, size); + } + + if (map.hasArray()) + return new ByteArrayWindow(this, pos, map.array()); + return new ByteBufferWindow(this, pos, map); + } + + private void onOpenPack() throws IOException { + final PackIndex idx = idx(); + final byte[] buf = new byte[20]; + + NB.readFully(fd.getChannel(), 0, buf, 0, 12); + if (RawParseUtils.match(buf, 0, Constants.PACK_SIGNATURE) != 4) + throw new IOException("Not a PACK file."); + final long vers = NB.decodeUInt32(buf, 4); + final long packCnt = NB.decodeUInt32(buf, 8); + if (vers != 2 && vers != 3) + throw new IOException("Unsupported pack version " + vers + "."); + + if (packCnt != idx.getObjectCount()) + throw new PackMismatchException("Pack object count mismatch:" + + " pack " + packCnt + + " index " + idx.getObjectCount() + + ": " + getPackFile()); + + NB.readFully(fd.getChannel(), length - 20, buf, 0, 20); + if (!Arrays.equals(buf, packChecksum)) + throw new PackMismatchException("Pack checksum mismatch:" + + " pack " + ObjectId.fromRaw(buf).name() + + " index " + ObjectId.fromRaw(idx.packChecksum).name() + + ": " + getPackFile()); + } + + private PackedObjectLoader reader(final WindowCursor curs, + final long objOffset) throws IOException { + long pos = objOffset; + int p = 0; + final byte[] ib = curs.tempId; + readFully(pos, ib, 0, 20, curs); + int c = ib[p++] & 0xff; + final int typeCode = (c >> 4) & 7; + long dataSize = c & 15; + int shift = 4; + while ((c & 0x80) != 0) { + c = ib[p++] & 0xff; + dataSize += (c & 0x7f) << shift; + shift += 7; + } + pos += p; + + switch (typeCode) { + case Constants.OBJ_COMMIT: + case Constants.OBJ_TREE: + case Constants.OBJ_BLOB: + case Constants.OBJ_TAG: + return new WholePackedObjectLoader(this, pos, objOffset, typeCode, + (int) dataSize); + + case Constants.OBJ_OFS_DELTA: { + readFully(pos, ib, 0, 20, curs); + p = 0; + c = ib[p++] & 0xff; + long ofs = c & 127; + while ((c & 128) != 0) { + ofs += 1; + c = ib[p++] & 0xff; + ofs <<= 7; + ofs += (c & 127); + } + return new DeltaOfsPackedObjectLoader(this, pos + p, objOffset, + (int) dataSize, objOffset - ofs); + } + case Constants.OBJ_REF_DELTA: { + readFully(pos, ib, 0, 20, curs); + return new DeltaRefPackedObjectLoader(this, pos + ib.length, + objOffset, (int) dataSize, ObjectId.fromRaw(ib)); + } + default: + throw new IOException("Unknown object type " + typeCode + "."); + } + } + + private long findEndOffset(final long startOffset) + throws IOException, CorruptObjectException { + final long maxOffset = length - 20; + return getReverseIdx().findNextOffset(startOffset, maxOffset); + } + + private synchronized PackReverseIndex getReverseIdx() throws IOException { + if (reverseIdx == null) + reverseIdx = new PackReverseIndex(idx()); + return reverseIdx; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndex.java new file mode 100644 index 0000000000..733834e5be --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndex.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Iterator; + +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.util.NB; + +/** + * Access path to locate objects by {@link ObjectId} in a {@link PackFile}. + * <p> + * Indexes are strictly redundant information in that we can rebuild all of the + * data held in the index file from the on disk representation of the pack file + * itself, but it is faster to access for random requests because data is stored + * by ObjectId. + * </p> + */ +public abstract class PackIndex implements Iterable<PackIndex.MutableEntry> { + /** + * Open an existing pack <code>.idx</code> file for reading. + * <p> + * The format of the file will be automatically detected and a proper access + * implementation for that format will be constructed and returned to the + * caller. The file may or may not be held open by the returned instance. + * </p> + * + * @param idxFile + * existing pack .idx to read. + * @return access implementation for the requested file. + * @throws FileNotFoundException + * the file does not exist. + * @throws IOException + * the file exists but could not be read due to security errors, + * unrecognized data version, or unexpected data corruption. + */ + public static PackIndex open(final File idxFile) throws IOException { + final FileInputStream fd = new FileInputStream(idxFile); + try { + final byte[] hdr = new byte[8]; + NB.readFully(fd, hdr, 0, hdr.length); + if (isTOC(hdr)) { + final int v = NB.decodeInt32(hdr, 4); + switch (v) { + case 2: + return new PackIndexV2(fd); + default: + throw new IOException("Unsupported pack index version " + v); + } + } + return new PackIndexV1(fd, hdr); + } catch (IOException ioe) { + final String path = idxFile.getAbsolutePath(); + final IOException err; + err = new IOException("Unreadable pack index: " + path); + err.initCause(ioe); + throw err; + } finally { + try { + fd.close(); + } catch (IOException err2) { + // ignore + } + } + } + + private static boolean isTOC(final byte[] h) { + final byte[] toc = PackIndexWriter.TOC; + for (int i = 0; i < toc.length; i++) + if (h[i] != toc[i]) + return false; + return true; + } + + /** Footer checksum applied on the bottom of the pack file. */ + protected byte[] packChecksum; + + /** + * Determine if an object is contained within the pack file. + * + * @param id + * the object to look for. Must not be null. + * @return true if the object is listed in this index; false otherwise. + */ + public boolean hasObject(final AnyObjectId id) { + return findOffset(id) != -1; + } + + /** + * Provide iterator that gives access to index entries. Note, that iterator + * returns reference to mutable object, the same reference in each call - + * for performance reason. If client needs immutable objects, it must copy + * returned object on its own. + * <p> + * Iterator returns objects in SHA-1 lexicographical order. + * </p> + * + * @return iterator over pack index entries + */ + public abstract Iterator<MutableEntry> iterator(); + + /** + * Obtain the total number of objects described by this index. + * + * @return number of objects in this index, and likewise in the associated + * pack that this index was generated from. + */ + abstract long getObjectCount(); + + /** + * Obtain the total number of objects needing 64 bit offsets. + * + * @return number of objects in this index using a 64 bit offset; that is an + * object positioned after the 2 GB position within the file. + */ + abstract long getOffset64Count(); + + /** + * Get ObjectId for the n-th object entry returned by {@link #iterator()}. + * <p> + * This method is a constant-time replacement for the following loop: + * + * <pre> + * Iterator<MutableEntry> eItr = index.iterator(); + * int curPosition = 0; + * while (eItr.hasNext() && curPosition++ < nthPosition) + * eItr.next(); + * ObjectId result = eItr.next().toObjectId(); + * </pre> + * + * @param nthPosition + * position within the traversal of {@link #iterator()} that the + * caller needs the object for. The first returned + * {@link MutableEntry} is 0, the second is 1, etc. + * @return the ObjectId for the corresponding entry. + */ + abstract ObjectId getObjectId(long nthPosition); + + /** + * Get ObjectId for the n-th object entry returned by {@link #iterator()}. + * <p> + * This method is a constant-time replacement for the following loop: + * + * <pre> + * Iterator<MutableEntry> eItr = index.iterator(); + * int curPosition = 0; + * while (eItr.hasNext() && curPosition++ < nthPosition) + * eItr.next(); + * ObjectId result = eItr.next().toObjectId(); + * </pre> + * + * @param nthPosition + * unsigned 32 bit position within the traversal of + * {@link #iterator()} that the caller needs the object for. The + * first returned {@link MutableEntry} is 0, the second is 1, + * etc. Positions past 2**31-1 are negative, but still valid. + * @return the ObjectId for the corresponding entry. + */ + final ObjectId getObjectId(final int nthPosition) { + if (nthPosition >= 0) + return getObjectId((long) nthPosition); + final int u31 = nthPosition >>> 1; + final int one = nthPosition & 1; + return getObjectId(((long) u31) << 1 | one); + } + + /** + * Locate the file offset position for the requested object. + * + * @param objId + * name of the object to locate within the pack. + * @return offset of the object's header and compressed content; -1 if the + * object does not exist in this index and is thus not stored in the + * associated pack. + */ + abstract long findOffset(AnyObjectId objId); + + /** + * Retrieve stored CRC32 checksum of the requested object raw-data + * (including header). + * + * @param objId + * id of object to look for + * @return CRC32 checksum of specified object (at 32 less significant bits) + * @throws MissingObjectException + * when requested ObjectId was not found in this index + * @throws UnsupportedOperationException + * when this index doesn't support CRC32 checksum + */ + abstract long findCRC32(AnyObjectId objId) throws MissingObjectException, + UnsupportedOperationException; + + /** + * Check whether this index supports (has) CRC32 checksums for objects. + * + * @return true if CRC32 is stored, false otherwise + */ + abstract boolean hasCRC32Support(); + + /** + * Represent mutable entry of pack index consisting of object id and offset + * in pack (both mutable). + * + */ + public static class MutableEntry { + final MutableObjectId idBuffer = new MutableObjectId(); + + long offset; + + /** + * Returns offset for this index object entry + * + * @return offset of this object in a pack file + */ + public long getOffset() { + return offset; + } + + /** @return hex string describing the object id of this entry. */ + public String name() { + ensureId(); + return idBuffer.name(); + } + + /** @return a copy of the object id. */ + public ObjectId toObjectId() { + ensureId(); + return idBuffer.toObjectId(); + } + + /** @return a complete copy of this entry, that won't modify */ + public MutableEntry cloneEntry() { + final MutableEntry r = new MutableEntry(); + ensureId(); + r.idBuffer.w1 = idBuffer.w1; + r.idBuffer.w2 = idBuffer.w2; + r.idBuffer.w3 = idBuffer.w3; + r.idBuffer.w4 = idBuffer.w4; + r.idBuffer.w5 = idBuffer.w5; + r.offset = offset; + return r; + } + + void ensureId() { + // Override in implementations. + } + } + + abstract class EntriesIterator implements Iterator<MutableEntry> { + protected final MutableEntry entry = initEntry(); + + protected long returnedNumber = 0; + + protected abstract MutableEntry initEntry(); + + public boolean hasNext() { + return returnedNumber < getObjectCount(); + } + + /** + * Implementation must update {@link #returnedNumber} before returning + * element. + */ + public abstract MutableEntry next(); + + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexV1.java new file mode 100644 index 0000000000..a7bf99e2fe --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexV1.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.util.NB; + +class PackIndexV1 extends PackIndex { + private static final int IDX_HDR_LEN = 256 * 4; + + private final long[] idxHeader; + + private byte[][] idxdata; + + private long objectCnt; + + PackIndexV1(final InputStream fd, final byte[] hdr) + throws CorruptObjectException, IOException { + final byte[] fanoutTable = new byte[IDX_HDR_LEN]; + System.arraycopy(hdr, 0, fanoutTable, 0, hdr.length); + NB.readFully(fd, fanoutTable, hdr.length, IDX_HDR_LEN - hdr.length); + + idxHeader = new long[256]; // really unsigned 32-bit... + for (int k = 0; k < idxHeader.length; k++) + idxHeader[k] = NB.decodeUInt32(fanoutTable, k * 4); + idxdata = new byte[idxHeader.length][]; + for (int k = 0; k < idxHeader.length; k++) { + int n; + if (k == 0) { + n = (int) (idxHeader[k]); + } else { + n = (int) (idxHeader[k] - idxHeader[k - 1]); + } + if (n > 0) { + idxdata[k] = new byte[n * (Constants.OBJECT_ID_LENGTH + 4)]; + NB.readFully(fd, idxdata[k], 0, idxdata[k].length); + } + } + objectCnt = idxHeader[255]; + + packChecksum = new byte[20]; + NB.readFully(fd, packChecksum, 0, packChecksum.length); + } + + long getObjectCount() { + return objectCnt; + } + + @Override + long getOffset64Count() { + long n64 = 0; + for (final MutableEntry e : this) { + if (e.getOffset() >= Integer.MAX_VALUE) + n64++; + } + return n64; + } + + @Override + ObjectId getObjectId(final long nthPosition) { + int levelOne = Arrays.binarySearch(idxHeader, nthPosition + 1); + long base; + if (levelOne >= 0) { + // If we hit the bucket exactly the item is in the bucket, or + // any bucket before it which has the same object count. + // + base = idxHeader[levelOne]; + while (levelOne > 0 && base == idxHeader[levelOne - 1]) + levelOne--; + } else { + // The item is in the bucket we would insert it into. + // + levelOne = -(levelOne + 1); + } + + base = levelOne > 0 ? idxHeader[levelOne - 1] : 0; + final int p = (int) (nthPosition - base); + final int dataIdx = ((4 + Constants.OBJECT_ID_LENGTH) * p) + 4; + return ObjectId.fromRaw(idxdata[levelOne], dataIdx); + } + + long findOffset(final AnyObjectId objId) { + final int levelOne = objId.getFirstByte(); + byte[] data = idxdata[levelOne]; + if (data == null) + return -1; + int high = data.length / (4 + Constants.OBJECT_ID_LENGTH); + int low = 0; + do { + final int mid = (low + high) >>> 1; + final int pos = ((4 + Constants.OBJECT_ID_LENGTH) * mid) + 4; + final int cmp = objId.compareTo(data, pos); + if (cmp < 0) + high = mid; + else if (cmp == 0) { + int b0 = data[pos - 4] & 0xff; + int b1 = data[pos - 3] & 0xff; + int b2 = data[pos - 2] & 0xff; + int b3 = data[pos - 1] & 0xff; + return (((long) b0) << 24) | (b1 << 16) | (b2 << 8) | (b3); + } else + low = mid + 1; + } while (low < high); + return -1; + } + + @Override + long findCRC32(AnyObjectId objId) { + throw new UnsupportedOperationException(); + } + + @Override + boolean hasCRC32Support() { + return false; + } + + public Iterator<MutableEntry> iterator() { + return new IndexV1Iterator(); + } + + private class IndexV1Iterator extends EntriesIterator { + private int levelOne; + + private int levelTwo; + + @Override + protected MutableEntry initEntry() { + return new MutableEntry() { + protected void ensureId() { + idBuffer.fromRaw(idxdata[levelOne], levelTwo + - Constants.OBJECT_ID_LENGTH); + } + }; + } + + public MutableEntry next() { + for (; levelOne < idxdata.length; levelOne++) { + if (idxdata[levelOne] == null) + continue; + if (levelTwo < idxdata[levelOne].length) { + entry.offset = NB.decodeUInt32(idxdata[levelOne], levelTwo); + levelTwo += Constants.OBJECT_ID_LENGTH + 4; + returnedNumber++; + return entry; + } + levelTwo = 0; + } + throw new NoSuchElementException(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexV2.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexV2.java new file mode 100644 index 0000000000..c37ce646de --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexV2.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.util.NB; + +/** Support for the pack index v2 format. */ +class PackIndexV2 extends PackIndex { + private static final long IS_O64 = 1L << 31; + + private static final int FANOUT = 256; + + private static final int[] NO_INTS = {}; + + private static final byte[] NO_BYTES = {}; + + private long objectCnt; + + private final long[] fanoutTable; + + /** 256 arrays of contiguous object names. */ + private int[][] names; + + /** 256 arrays of the 32 bit offset data, matching {@link #names}. */ + private byte[][] offset32; + + /** 256 arrays of the CRC-32 of objects, matching {@link #names}. */ + private byte[][] crc32; + + /** 64 bit offset table. */ + private byte[] offset64; + + PackIndexV2(final InputStream fd) throws IOException { + final byte[] fanoutRaw = new byte[4 * FANOUT]; + NB.readFully(fd, fanoutRaw, 0, fanoutRaw.length); + fanoutTable = new long[FANOUT]; + for (int k = 0; k < FANOUT; k++) + fanoutTable[k] = NB.decodeUInt32(fanoutRaw, k * 4); + objectCnt = fanoutTable[FANOUT - 1]; + + names = new int[FANOUT][]; + offset32 = new byte[FANOUT][]; + crc32 = new byte[FANOUT][]; + + // Object name table. The size we can permit per fan-out bucket + // is limited to Java's 2 GB per byte array limitation. That is + // no more than 107,374,182 objects per fan-out. + // + for (int k = 0; k < FANOUT; k++) { + final long bucketCnt; + if (k == 0) + bucketCnt = fanoutTable[k]; + else + bucketCnt = fanoutTable[k] - fanoutTable[k - 1]; + + if (bucketCnt == 0) { + names[k] = NO_INTS; + offset32[k] = NO_BYTES; + crc32[k] = NO_BYTES; + continue; + } + + final long nameLen = bucketCnt * Constants.OBJECT_ID_LENGTH; + if (nameLen > Integer.MAX_VALUE) + throw new IOException("Index file is too large for jgit"); + + final int intNameLen = (int) nameLen; + final byte[] raw = new byte[intNameLen]; + final int[] bin = new int[intNameLen >>> 2]; + NB.readFully(fd, raw, 0, raw.length); + for (int i = 0; i < bin.length; i++) + bin[i] = NB.decodeInt32(raw, i << 2); + + names[k] = bin; + offset32[k] = new byte[(int) (bucketCnt * 4)]; + crc32[k] = new byte[(int) (bucketCnt * 4)]; + } + + // CRC32 table. + for (int k = 0; k < FANOUT; k++) + NB.readFully(fd, crc32[k], 0, crc32[k].length); + + // 32 bit offset table. Any entries with the most significant bit + // set require a 64 bit offset entry in another table. + // + int o64cnt = 0; + for (int k = 0; k < FANOUT; k++) { + final byte[] ofs = offset32[k]; + NB.readFully(fd, ofs, 0, ofs.length); + for (int p = 0; p < ofs.length; p += 4) + if (ofs[p] < 0) + o64cnt++; + } + + // 64 bit offset table. Most objects should not require an entry. + // + if (o64cnt > 0) { + offset64 = new byte[o64cnt * 8]; + NB.readFully(fd, offset64, 0, offset64.length); + } else { + offset64 = NO_BYTES; + } + + packChecksum = new byte[20]; + NB.readFully(fd, packChecksum, 0, packChecksum.length); + } + + @Override + long getObjectCount() { + return objectCnt; + } + + @Override + long getOffset64Count() { + return offset64.length / 8; + } + + @Override + ObjectId getObjectId(final long nthPosition) { + int levelOne = Arrays.binarySearch(fanoutTable, nthPosition + 1); + long base; + if (levelOne >= 0) { + // If we hit the bucket exactly the item is in the bucket, or + // any bucket before it which has the same object count. + // + base = fanoutTable[levelOne]; + while (levelOne > 0 && base == fanoutTable[levelOne - 1]) + levelOne--; + } else { + // The item is in the bucket we would insert it into. + // + levelOne = -(levelOne + 1); + } + + base = levelOne > 0 ? fanoutTable[levelOne - 1] : 0; + final int p = (int) (nthPosition - base); + final int p4 = p << 2; + return ObjectId.fromRaw(names[levelOne], p4 + p); // p * 5 + } + + @Override + long findOffset(final AnyObjectId objId) { + final int levelOne = objId.getFirstByte(); + final int levelTwo = binarySearchLevelTwo(objId, levelOne); + if (levelTwo == -1) + return -1; + final long p = NB.decodeUInt32(offset32[levelOne], levelTwo << 2); + if ((p & IS_O64) != 0) + return NB.decodeUInt64(offset64, (8 * (int) (p & ~IS_O64))); + return p; + } + + @Override + long findCRC32(AnyObjectId objId) throws MissingObjectException { + final int levelOne = objId.getFirstByte(); + final int levelTwo = binarySearchLevelTwo(objId, levelOne); + if (levelTwo == -1) + throw new MissingObjectException(objId.copy(), "unknown"); + return NB.decodeUInt32(crc32[levelOne], levelTwo << 2); + } + + @Override + boolean hasCRC32Support() { + return true; + } + + public Iterator<MutableEntry> iterator() { + return new EntriesIteratorV2(); + } + + private int binarySearchLevelTwo(final AnyObjectId objId, final int levelOne) { + final int[] data = names[levelOne]; + int high = offset32[levelOne].length >>> 2; + if (high == 0) + return -1; + int low = 0; + do { + final int mid = (low + high) >>> 1; + final int mid4 = mid << 2; + final int cmp; + + cmp = objId.compareTo(data, mid4 + mid); // mid * 5 + if (cmp < 0) + high = mid; + else if (cmp == 0) { + return mid; + } else + low = mid + 1; + } while (low < high); + return -1; + } + + private class EntriesIteratorV2 extends EntriesIterator { + private int levelOne; + + private int levelTwo; + + @Override + protected MutableEntry initEntry() { + return new MutableEntry() { + protected void ensureId() { + idBuffer.fromRaw(names[levelOne], levelTwo + - Constants.OBJECT_ID_LENGTH / 4); + } + }; + } + + public MutableEntry next() { + for (; levelOne < names.length; levelOne++) { + if (levelTwo < names[levelOne].length) { + int idx = levelTwo / (Constants.OBJECT_ID_LENGTH / 4) * 4; + long offset = NB.decodeUInt32(offset32[levelOne], idx); + if ((offset & IS_O64) != 0) { + idx = (8 * (int) (offset & ~IS_O64)); + offset = NB.decodeUInt64(offset64, idx); + } + entry.offset = offset; + + levelTwo += Constants.OBJECT_ID_LENGTH / 4; + returnedNumber++; + return entry; + } + levelTwo = 0; + } + throw new NoSuchElementException(); + } + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexWriter.java new file mode 100644 index 0000000000..5fcf71a781 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexWriter.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.DigestOutputStream; +import java.util.List; + +import org.eclipse.jgit.transport.PackedObjectInfo; +import org.eclipse.jgit.util.NB; + +/** + * Creates a table of contents to support random access by {@link PackFile}. + * <p> + * Pack index files (the <code>.idx</code> suffix in a pack file pair) + * provides random access to any object in the pack by associating an ObjectId + * to the byte offset within the pack where the object's data can be read. + */ +public abstract class PackIndexWriter { + /** Magic constant indicating post-version 1 format. */ + protected static final byte[] TOC = { -1, 't', 'O', 'c' }; + + /** + * Create a new writer for the oldest (most widely understood) format. + * <p> + * This method selects an index format that can accurate describe the + * supplied objects and that will be the most compatible format with older + * Git implementations. + * <p> + * Index version 1 is widely recognized by all Git implementations, but + * index version 2 (and later) is not as well recognized as it was + * introduced more than a year later. Index version 1 can only be used if + * the resulting pack file is under 4 gigabytes in size; packs larger than + * that limit must use index version 2. + * + * @param dst + * the stream the index data will be written to. If not already + * buffered it will be automatically wrapped in a buffered + * stream. Callers are always responsible for closing the stream. + * @param objs + * the objects the caller needs to store in the index. Entries + * will be examined until a format can be conclusively selected. + * @return a new writer to output an index file of the requested format to + * the supplied stream. + * @throws IllegalArgumentException + * no recognized pack index version can support the supplied + * objects. This is likely a bug in the implementation. + */ + @SuppressWarnings("fallthrough") + public static PackIndexWriter createOldestPossible(final OutputStream dst, + final List<? extends PackedObjectInfo> objs) { + int version = 1; + LOOP: for (final PackedObjectInfo oe : objs) { + switch (version) { + case 1: + if (PackIndexWriterV1.canStore(oe)) + continue; + version = 2; + case 2: + break LOOP; + } + } + return createVersion(dst, version); + } + + /** + * Create a new writer instance for a specific index format version. + * + * @param dst + * the stream the index data will be written to. If not already + * buffered it will be automatically wrapped in a buffered + * stream. Callers are always responsible for closing the stream. + * @param version + * index format version number required by the caller. Exactly + * this formatted version will be written. + * @return a new writer to output an index file of the requested format to + * the supplied stream. + * @throws IllegalArgumentException + * the version requested is not supported by this + * implementation. + */ + public static PackIndexWriter createVersion(final OutputStream dst, + final int version) { + switch (version) { + case 1: + return new PackIndexWriterV1(dst); + case 2: + return new PackIndexWriterV2(dst); + default: + throw new IllegalArgumentException( + "Unsupported pack index version " + version); + } + } + + /** The index data stream we are responsible for creating. */ + protected final DigestOutputStream out; + + /** A temporary buffer for use during IO to {link #out}. */ + protected final byte[] tmp; + + /** The entries this writer must pack. */ + protected List<? extends PackedObjectInfo> entries; + + /** SHA-1 checksum for the entire pack data. */ + protected byte[] packChecksum; + + /** + * Create a new writer instance. + * + * @param dst + * the stream this instance outputs to. If not already buffered + * it will be automatically wrapped in a buffered stream. + */ + protected PackIndexWriter(final OutputStream dst) { + out = new DigestOutputStream(dst instanceof BufferedOutputStream ? dst + : new BufferedOutputStream(dst), Constants.newMessageDigest()); + tmp = new byte[4 + Constants.OBJECT_ID_LENGTH]; + } + + /** + * Write all object entries to the index stream. + * <p> + * After writing the stream passed to the factory is flushed but remains + * open. Callers are always responsible for closing the output stream. + * + * @param toStore + * sorted list of objects to store in the index. The caller must + * have previously sorted the list using {@link PackedObjectInfo}'s + * native {@link Comparable} implementation. + * @param packDataChecksum + * checksum signature of the entire pack data content. This is + * traditionally the last 20 bytes of the pack file's own stream. + * @throws IOException + * an error occurred while writing to the output stream, or this + * index format cannot store the object data supplied. + */ + public void write(final List<? extends PackedObjectInfo> toStore, + final byte[] packDataChecksum) throws IOException { + entries = toStore; + packChecksum = packDataChecksum; + writeImpl(); + out.flush(); + } + + /** + * Writes the index file to {@link #out}. + * <p> + * Implementations should go something like: + * + * <pre> + * writeFanOutTable(); + * for (final PackedObjectInfo po : entries) + * writeOneEntry(po); + * writeChecksumFooter(); + * </pre> + * + * <p> + * Where the logic for <code>writeOneEntry</code> is specific to the index + * format in use. Additional headers/footers may be used if necessary and + * the {@link #entries} collection may be iterated over more than once if + * necessary. Implementors therefore have complete control over the data. + * + * @throws IOException + * an error occurred while writing to the output stream, or this + * index format cannot store the object data supplied. + */ + protected abstract void writeImpl() throws IOException; + + /** + * Output the version 2 (and later) TOC header, with version number. + * <p> + * Post version 1 all index files start with a TOC header that makes the + * file an invalid version 1 file, and then includes the version number. + * This header is necessary to recognize a version 1 from a version 2 + * formatted index. + * + * @param version + * version number of this index format being written. + * @throws IOException + * an error occurred while writing to the output stream. + */ + protected void writeTOC(final int version) throws IOException { + out.write(TOC); + NB.encodeInt32(tmp, 0, version); + out.write(tmp, 0, 4); + } + + /** + * Output the standard 256 entry first-level fan-out table. + * <p> + * The fan-out table is 4 KB in size, holding 256 32-bit unsigned integer + * counts. Each count represents the number of objects within this index + * whose {@link ObjectId#getFirstByte()} matches the count's position in the + * fan-out table. + * + * @throws IOException + * an error occurred while writing to the output stream. + */ + protected void writeFanOutTable() throws IOException { + final int[] fanout = new int[256]; + for (final PackedObjectInfo po : entries) + fanout[po.getFirstByte() & 0xff]++; + for (int i = 1; i < 256; i++) + fanout[i] += fanout[i - 1]; + for (final int n : fanout) { + NB.encodeInt32(tmp, 0, n); + out.write(tmp, 0, 4); + } + } + + /** + * Output the standard two-checksum index footer. + * <p> + * The standard footer contains two checksums (20 byte SHA-1 values): + * <ol> + * <li>Pack data checksum - taken from the last 20 bytes of the pack file.</li> + * <li>Index data checksum - checksum of all index bytes written, including + * the pack data checksum above.</li> + * </ol> + * + * @throws IOException + * an error occurred while writing to the output stream. + */ + protected void writeChecksumFooter() throws IOException { + out.write(packChecksum); + out.on(false); + out.write(out.getMessageDigest().digest()); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexWriterV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexWriterV1.java new file mode 100644 index 0000000000..b3be5480c9 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexWriterV1.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.io.OutputStream; + +import org.eclipse.jgit.transport.PackedObjectInfo; +import org.eclipse.jgit.util.NB; + +/** + * Creates the version 1 (old style) pack table of contents files. + * + * @see PackIndexWriter + * @see PackIndexV1 + */ +class PackIndexWriterV1 extends PackIndexWriter { + static boolean canStore(final PackedObjectInfo oe) { + // We are limited to 4 GB per pack as offset is 32 bit unsigned int. + // + return oe.getOffset() >>> 1 < Integer.MAX_VALUE; + } + + PackIndexWriterV1(final OutputStream dst) { + super(dst); + } + + @Override + protected void writeImpl() throws IOException { + writeFanOutTable(); + + for (final PackedObjectInfo oe : entries) { + if (!canStore(oe)) + throw new IOException("Pack too large for index version 1"); + NB.encodeInt32(tmp, 0, (int) oe.getOffset()); + oe.copyRawTo(tmp, 4); + out.write(tmp); + } + + writeChecksumFooter(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexWriterV2.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexWriterV2.java new file mode 100644 index 0000000000..b6ac7b89e3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackIndexWriterV2.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.io.OutputStream; + +import org.eclipse.jgit.transport.PackedObjectInfo; +import org.eclipse.jgit.util.NB; + +/** + * Creates the version 2 pack table of contents files. + * + * @see PackIndexWriter + * @see PackIndexV2 + */ +class PackIndexWriterV2 extends PackIndexWriter { + PackIndexWriterV2(final OutputStream dst) { + super(dst); + } + + @Override + protected void writeImpl() throws IOException { + writeTOC(2); + writeFanOutTable(); + writeObjectNames(); + writeCRCs(); + writeOffset32(); + writeOffset64(); + writeChecksumFooter(); + } + + private void writeObjectNames() throws IOException { + for (final PackedObjectInfo oe : entries) + oe.copyRawTo(out); + } + + private void writeCRCs() throws IOException { + for (final PackedObjectInfo oe : entries) { + NB.encodeInt32(tmp, 0, oe.getCRC()); + out.write(tmp, 0, 4); + } + } + + private void writeOffset32() throws IOException { + int o64 = 0; + for (final PackedObjectInfo oe : entries) { + final long o = oe.getOffset(); + if (o < Integer.MAX_VALUE) + NB.encodeInt32(tmp, 0, (int) o); + else + NB.encodeInt32(tmp, 0, (1 << 31) | o64++); + out.write(tmp, 0, 4); + } + } + + private void writeOffset64() throws IOException { + for (final PackedObjectInfo oe : entries) { + final long o = oe.getOffset(); + if (o > Integer.MAX_VALUE) { + NB.encodeInt64(tmp, 0, o); + out.write(tmp, 0, 8); + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackLock.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackLock.java new file mode 100644 index 0000000000..de8e3fa637 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackLock.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.IOException; + +/** Keeps track of a {@link PackFile}'s associated <code>.keep</code> file. */ +public class PackLock { + private final File keepFile; + + /** + * Create a new lock for a pack file. + * + * @param packFile + * location of the <code>pack-*.pack</code> file. + */ + public PackLock(final File packFile) { + final File p = packFile.getParentFile(); + final String n = packFile.getName(); + keepFile = new File(p, n.substring(0, n.length() - 5) + ".keep"); + } + + /** + * Create the <code>pack-*.keep</code> file, with the given message. + * + * @param msg + * message to store in the file. + * @return true if the keep file was successfully written; false otherwise. + * @throws IOException + * the keep file could not be written. + */ + public boolean lock(String msg) throws IOException { + if (msg == null) + return false; + if (!msg.endsWith("\n")) + msg += "\n"; + final LockFile lf = new LockFile(keepFile); + if (!lf.lock()) + return false; + lf.write(Constants.encode(msg)); + return lf.commit(); + } + + /** Remove the <code>.keep</code> file that holds this pack in place. */ + public void unlock() { + keepFile.delete(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackOutputStream.java new file mode 100644 index 0000000000..a348f1e547 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackOutputStream.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.util.zip.CRC32; + +/** Custom output stream to support {@link PackWriter}. */ +final class PackOutputStream extends OutputStream { + private final OutputStream out; + + private final CRC32 crc = new CRC32(); + + private final MessageDigest md = Constants.newMessageDigest(); + + private long count; + + PackOutputStream(final OutputStream out) { + this.out = out; + } + + @Override + public void write(final int b) throws IOException { + out.write(b); + crc.update(b); + md.update((byte) b); + count++; + } + + @Override + public void write(final byte[] b, final int off, final int len) + throws IOException { + out.write(b, off, len); + crc.update(b, off, len); + md.update(b, off, len); + count += len; + } + + @Override + public void flush() throws IOException { + out.flush(); + } + + /** @return total number of bytes written since stream start. */ + long length() { + return count; + } + + /** @return obtain the current CRC32 register. */ + int getCRC32() { + return (int) crc.getValue(); + } + + /** Reinitialize the CRC32 register for a new region. */ + void resetCRC32() { + crc.reset(); + } + + /** @return obtain the current SHA-1 digest. */ + byte[] getDigest() { + return md.digest(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackReverseIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackReverseIndex.java new file mode 100644 index 0000000000..c0ed7b29a6 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackReverseIndex.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.util.Arrays; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.lib.PackIndex.MutableEntry; + +/** + * <p> + * Reverse index for forward pack index. Provides operations based on offset + * instead of object id. Such offset-based reverse lookups are performed in + * O(log n) time. + * </p> + * + * @see PackIndex + * @see PackFile + */ +class PackReverseIndex { + /** Index we were created from, and that has our ObjectId data. */ + private final PackIndex index; + + /** + * (offset31, truly) Offsets accommodating in 31 bits. + */ + private final int offsets32[]; + + /** + * Offsets not accommodating in 31 bits. + */ + private final long offsets64[]; + + /** Position of the corresponding {@link #offsets32} in {@link #index}. */ + private final int nth32[]; + + /** Position of the corresponding {@link #offsets64} in {@link #index}. */ + private final int nth64[]; + + /** + * Create reverse index from straight/forward pack index, by indexing all + * its entries. + * + * @param packIndex + * forward index - entries to (reverse) index. + */ + PackReverseIndex(final PackIndex packIndex) { + index = packIndex; + + final long cnt = index.getObjectCount(); + final long n64 = index.getOffset64Count(); + final long n32 = cnt - n64; + if (n32 > Integer.MAX_VALUE || n64 > Integer.MAX_VALUE + || cnt > 0xffffffffL) + throw new IllegalArgumentException( + "Huge indexes are not supported by jgit, yet"); + + offsets32 = new int[(int) n32]; + offsets64 = new long[(int) n64]; + nth32 = new int[offsets32.length]; + nth64 = new int[offsets64.length]; + + int i32 = 0; + int i64 = 0; + for (final MutableEntry me : index) { + final long o = me.getOffset(); + if (o < Integer.MAX_VALUE) + offsets32[i32++] = (int) o; + else + offsets64[i64++] = o; + } + + Arrays.sort(offsets32); + Arrays.sort(offsets64); + + int nth = 0; + for (final MutableEntry me : index) { + final long o = me.getOffset(); + if (o < Integer.MAX_VALUE) + nth32[Arrays.binarySearch(offsets32, (int) o)] = nth++; + else + nth64[Arrays.binarySearch(offsets64, o)] = nth++; + } + } + + /** + * Search for object id with the specified start offset in this pack + * (reverse) index. + * + * @param offset + * start offset of object to find. + * @return object id for this offset, or null if no object was found. + */ + ObjectId findObject(final long offset) { + if (offset <= Integer.MAX_VALUE) { + final int i32 = Arrays.binarySearch(offsets32, (int) offset); + if (i32 < 0) + return null; + return index.getObjectId(nth32[i32]); + } else { + final int i64 = Arrays.binarySearch(offsets64, offset); + if (i64 < 0) + return null; + return index.getObjectId(nth64[i64]); + } + } + + /** + * Search for the next offset to the specified offset in this pack (reverse) + * index. + * + * @param offset + * start offset of previous object (must be valid-existing + * offset). + * @param maxOffset + * maximum offset in a pack (returned when there is no next + * offset). + * @return offset of the next object in a pack or maxOffset if provided + * offset was the last one. + * @throws CorruptObjectException + * when there is no object with the provided offset. + */ + long findNextOffset(final long offset, final long maxOffset) + throws CorruptObjectException { + if (offset <= Integer.MAX_VALUE) { + final int i32 = Arrays.binarySearch(offsets32, (int) offset); + if (i32 < 0) + throw new CorruptObjectException( + "Can't find object in (reverse) pack index for the specified offset " + + offset); + + if (i32 + 1 == offsets32.length) { + if (offsets64.length > 0) + return offsets64[0]; + return maxOffset; + } + return offsets32[i32 + 1]; + } else { + final int i64 = Arrays.binarySearch(offsets64, offset); + if (i64 < 0) + throw new CorruptObjectException( + "Can't find object in (reverse) pack index for the specified offset " + + offset); + + if (i64 + 1 == offsets64.length) + return maxOffset; + return offsets64[i64 + 1]; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackWriter.java new file mode 100644 index 0000000000..6162deab7f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackWriter.java @@ -0,0 +1,1045 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.zip.Deflater; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.revwalk.ObjectWalk; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevSort; +import org.eclipse.jgit.transport.PackedObjectInfo; +import org.eclipse.jgit.util.NB; + +/** + * <p> + * PackWriter class is responsible for generating pack files from specified set + * of objects from repository. This implementation produce pack files in format + * version 2. + * </p> + * <p> + * Source of objects may be specified in two ways: + * <ul> + * <li>(usually) by providing sets of interesting and uninteresting objects in + * repository - all interesting objects and their ancestors except uninteresting + * objects and their ancestors will be included in pack, or</li> + * <li>by providing iterator of {@link RevObject} specifying exact list and + * order of objects in pack</li> + * </ul> + * Typical usage consists of creating instance intended for some pack, + * configuring options, preparing the list of objects by calling + * {@link #preparePack(Iterator)} or + * {@link #preparePack(Collection, Collection)}, and finally + * producing the stream with {@link #writePack(OutputStream)}. + * </p> + * <p> + * Class provide set of configurable options and {@link ProgressMonitor} + * support, as operations may take a long time for big repositories. Deltas + * searching algorithm is <b>NOT IMPLEMENTED</b> yet - this implementation + * relies only on deltas and objects reuse. + * </p> + * <p> + * This class is not thread safe, it is intended to be used in one thread, with + * one instance per created pack. Subsequent calls to writePack result in + * undefined behavior. + * </p> + */ + +public class PackWriter { + /** + * Title of {@link ProgressMonitor} task used during counting objects to + * pack. + * + * @see #preparePack(Collection, Collection) + */ + public static final String COUNTING_OBJECTS_PROGRESS = "Counting objects"; + + /** + * Title of {@link ProgressMonitor} task used during searching for objects + * reuse or delta reuse. + * + * @see #writePack(OutputStream) + */ + public static final String SEARCHING_REUSE_PROGRESS = "Compressing objects"; + + /** + * Title of {@link ProgressMonitor} task used during writing out pack + * (objects) + * + * @see #writePack(OutputStream) + */ + public static final String WRITING_OBJECTS_PROGRESS = "Writing objects"; + + /** + * Default value of deltas reuse option. + * + * @see #setReuseDeltas(boolean) + */ + public static final boolean DEFAULT_REUSE_DELTAS = true; + + /** + * Default value of objects reuse option. + * + * @see #setReuseObjects(boolean) + */ + public static final boolean DEFAULT_REUSE_OBJECTS = true; + + /** + * Default value of delta base as offset option. + * + * @see #setDeltaBaseAsOffset(boolean) + */ + public static final boolean DEFAULT_DELTA_BASE_AS_OFFSET = false; + + /** + * Default value of maximum delta chain depth. + * + * @see #setMaxDeltaDepth(int) + */ + public static final int DEFAULT_MAX_DELTA_DEPTH = 50; + + private static final int PACK_VERSION_GENERATED = 2; + + @SuppressWarnings("unchecked") + private final List<ObjectToPack> objectsLists[] = new List[Constants.OBJ_TAG + 1]; + { + objectsLists[0] = Collections.<ObjectToPack> emptyList(); + objectsLists[Constants.OBJ_COMMIT] = new ArrayList<ObjectToPack>(); + objectsLists[Constants.OBJ_TREE] = new ArrayList<ObjectToPack>(); + objectsLists[Constants.OBJ_BLOB] = new ArrayList<ObjectToPack>(); + objectsLists[Constants.OBJ_TAG] = new ArrayList<ObjectToPack>(); + } + + private final ObjectIdSubclassMap<ObjectToPack> objectsMap = new ObjectIdSubclassMap<ObjectToPack>(); + + // edge objects for thin packs + private final ObjectIdSubclassMap<ObjectId> edgeObjects = new ObjectIdSubclassMap<ObjectId>(); + + private final Repository db; + + private PackOutputStream out; + + private final Deflater deflater; + + private ProgressMonitor initMonitor; + + private ProgressMonitor writeMonitor; + + private final byte[] buf = new byte[16384]; // 16 KB + + private final WindowCursor windowCursor = new WindowCursor(); + + private List<ObjectToPack> sortedByName; + + private byte packcsum[]; + + private boolean reuseDeltas = DEFAULT_REUSE_DELTAS; + + private boolean reuseObjects = DEFAULT_REUSE_OBJECTS; + + private boolean deltaBaseAsOffset = DEFAULT_DELTA_BASE_AS_OFFSET; + + private int maxDeltaDepth = DEFAULT_MAX_DELTA_DEPTH; + + private int outputVersion; + + private boolean thin; + + private boolean ignoreMissingUninteresting = true; + + /** + * Create writer for specified repository. + * <p> + * Objects for packing are specified in {@link #preparePack(Iterator)} or + * {@link #preparePack(Collection, Collection)}. + * + * @param repo + * repository where objects are stored. + * @param monitor + * operations progress monitor, used within + * {@link #preparePack(Iterator)}, + * {@link #preparePack(Collection, Collection)} + * , or {@link #writePack(OutputStream)}. + */ + public PackWriter(final Repository repo, final ProgressMonitor monitor) { + this(repo, monitor, monitor); + } + + /** + * Create writer for specified repository. + * <p> + * Objects for packing are specified in {@link #preparePack(Iterator)} or + * {@link #preparePack(Collection, Collection)}. + * + * @param repo + * repository where objects are stored. + * @param imonitor + * operations progress monitor, used within + * {@link #preparePack(Iterator)}, + * {@link #preparePack(Collection, Collection)} + * @param wmonitor + * operations progress monitor, used within + * {@link #writePack(OutputStream)}. + */ + public PackWriter(final Repository repo, final ProgressMonitor imonitor, + final ProgressMonitor wmonitor) { + this.db = repo; + initMonitor = imonitor == null ? NullProgressMonitor.INSTANCE : imonitor; + writeMonitor = wmonitor == null ? NullProgressMonitor.INSTANCE : wmonitor; + this.deflater = new Deflater(db.getConfig().getCore().getCompression()); + outputVersion = repo.getConfig().getCore().getPackIndexVersion(); + } + + /** + * Check whether object is configured to reuse deltas existing in + * repository. + * <p> + * Default setting: {@value #DEFAULT_REUSE_DELTAS} + * </p> + * + * @return true if object is configured to reuse deltas; false otherwise. + */ + public boolean isReuseDeltas() { + return reuseDeltas; + } + + /** + * Set reuse deltas configuration option for this writer. When enabled, + * writer will search for delta representation of object in repository and + * use it if possible. Normally, only deltas with base to another object + * existing in set of objects to pack will be used. Exception is however + * thin-pack (see + * {@link #preparePack(Collection, Collection)} and + * {@link #preparePack(Iterator)}) where base object must exist on other + * side machine. + * <p> + * When raw delta data is directly copied from a pack file, checksum is + * computed to verify data. + * </p> + * <p> + * Default setting: {@value #DEFAULT_REUSE_DELTAS} + * </p> + * + * @param reuseDeltas + * boolean indicating whether or not try to reuse deltas. + */ + public void setReuseDeltas(boolean reuseDeltas) { + this.reuseDeltas = reuseDeltas; + } + + /** + * Checks whether object is configured to reuse existing objects + * representation in repository. + * <p> + * Default setting: {@value #DEFAULT_REUSE_OBJECTS} + * </p> + * + * @return true if writer is configured to reuse objects representation from + * pack; false otherwise. + */ + public boolean isReuseObjects() { + return reuseObjects; + } + + /** + * Set reuse objects configuration option for this writer. If enabled, + * writer searches for representation in a pack file. If possible, + * compressed data is directly copied from such a pack file. Data checksum + * is verified. + * <p> + * Default setting: {@value #DEFAULT_REUSE_OBJECTS} + * </p> + * + * @param reuseObjects + * boolean indicating whether or not writer should reuse existing + * objects representation. + */ + public void setReuseObjects(boolean reuseObjects) { + this.reuseObjects = reuseObjects; + } + + /** + * Check whether writer can store delta base as an offset (new style + * reducing pack size) or should store it as an object id (legacy style, + * compatible with old readers). + * <p> + * Default setting: {@value #DEFAULT_DELTA_BASE_AS_OFFSET} + * </p> + * + * @return true if delta base is stored as an offset; false if it is stored + * as an object id. + */ + public boolean isDeltaBaseAsOffset() { + return deltaBaseAsOffset; + } + + /** + * Set writer delta base format. Delta base can be written as an offset in a + * pack file (new approach reducing file size) or as an object id (legacy + * approach, compatible with old readers). + * <p> + * Default setting: {@value #DEFAULT_DELTA_BASE_AS_OFFSET} + * </p> + * + * @param deltaBaseAsOffset + * boolean indicating whether delta base can be stored as an + * offset. + */ + public void setDeltaBaseAsOffset(boolean deltaBaseAsOffset) { + this.deltaBaseAsOffset = deltaBaseAsOffset; + } + + /** + * Get maximum depth of delta chain set up for this writer. Generated chains + * are not longer than this value. + * <p> + * Default setting: {@value #DEFAULT_MAX_DELTA_DEPTH} + * </p> + * + * @return maximum delta chain depth. + */ + public int getMaxDeltaDepth() { + return maxDeltaDepth; + } + + /** + * Set up maximum depth of delta chain for this writer. Generated chains are + * not longer than this value. Too low value causes low compression level, + * while too big makes unpacking (reading) longer. + * <p> + * Default setting: {@value #DEFAULT_MAX_DELTA_DEPTH} + * </p> + * + * @param maxDeltaDepth + * maximum delta chain depth. + */ + public void setMaxDeltaDepth(int maxDeltaDepth) { + this.maxDeltaDepth = maxDeltaDepth; + } + + /** @return true if this writer is producing a thin pack. */ + public boolean isThin() { + return thin; + } + + /** + * @param packthin + * a boolean indicating whether writer may pack objects with + * delta base object not within set of objects to pack, but + * belonging to party repository (uninteresting/boundary) as + * determined by set; this kind of pack is used only for + * transport; true - to produce thin pack, false - otherwise. + */ + public void setThin(final boolean packthin) { + thin = packthin; + } + + /** + * @return true to ignore objects that are uninteresting and also not found + * on local disk; false to throw a {@link MissingObjectException} + * out of {@link #preparePack(Collection, Collection)} if an + * uninteresting object is not in the source repository. By default, + * true, permitting gracefully ignoring of uninteresting objects. + */ + public boolean isIgnoreMissingUninteresting() { + return ignoreMissingUninteresting; + } + + /** + * @param ignore + * true if writer should ignore non existing uninteresting + * objects during construction set of objects to pack; false + * otherwise - non existing uninteresting objects may cause + * {@link MissingObjectException} + */ + public void setIgnoreMissingUninteresting(final boolean ignore) { + ignoreMissingUninteresting = ignore; + } + + /** + * Set the pack index file format version this instance will create. + * + * @param version + * the version to write. The special version 0 designates the + * oldest (most compatible) format available for the objects. + * @see PackIndexWriter + */ + public void setIndexVersion(final int version) { + outputVersion = version; + } + + /** + * Returns objects number in a pack file that was created by this writer. + * + * @return number of objects in pack. + */ + public int getObjectsNumber() { + return objectsMap.size(); + } + + /** + * Prepare the list of objects to be written to the pack stream. + * <p> + * Iterator <b>exactly</b> determines which objects are included in a pack + * and order they appear in pack (except that objects order by type is not + * needed at input). This order should conform general rules of ordering + * objects in git - by recency and path (type and delta-base first is + * internally secured) and responsibility for guaranteeing this order is on + * a caller side. Iterator must return each id of object to write exactly + * once. + * </p> + * <p> + * When iterator returns object that has {@link RevFlag#UNINTERESTING} flag, + * this object won't be included in an output pack. Instead, it is recorded + * as edge-object (known to remote repository) for thin-pack. In such a case + * writer may pack objects with delta base object not within set of objects + * to pack, but belonging to party repository - those marked with + * {@link RevFlag#UNINTERESTING} flag. This type of pack is used only for + * transport. + * </p> + * + * @param objectsSource + * iterator of object to store in a pack; order of objects within + * each type is important, ordering by type is not needed; + * allowed types for objects are {@link Constants#OBJ_COMMIT}, + * {@link Constants#OBJ_TREE}, {@link Constants#OBJ_BLOB} and + * {@link Constants#OBJ_TAG}; objects returned by iterator may + * be later reused by caller as object id and type are internally + * copied in each iteration; if object returned by iterator has + * {@link RevFlag#UNINTERESTING} flag set, it won't be included + * in a pack, but is considered as edge-object for thin-pack. + * @throws IOException + * when some I/O problem occur during reading objects. + */ + public void preparePack(final Iterator<RevObject> objectsSource) + throws IOException { + while (objectsSource.hasNext()) { + addObject(objectsSource.next()); + } + } + + /** + * Prepare the list of objects to be written to the pack stream. + * <p> + * Basing on these 2 sets, another set of objects to put in a pack file is + * created: this set consists of all objects reachable (ancestors) from + * interesting objects, except uninteresting objects and their ancestors. + * This method uses class {@link ObjectWalk} extensively to find out that + * appropriate set of output objects and their optimal order in output pack. + * Order is consistent with general git in-pack rules: sort by object type, + * recency, path and delta-base first. + * </p> + * + * @param interestingObjects + * collection of objects to be marked as interesting (start + * points of graph traversal). + * @param uninterestingObjects + * collection of objects to be marked as uninteresting (end + * points of graph traversal). + * @throws IOException + * when some I/O problem occur during reading objects. + */ + public void preparePack( + final Collection<? extends ObjectId> interestingObjects, + final Collection<? extends ObjectId> uninterestingObjects) + throws IOException { + ObjectWalk walker = setUpWalker(interestingObjects, + uninterestingObjects); + findObjectsToPack(walker); + } + + /** + * Determine if the pack file will contain the requested object. + * + * @param id + * the object to test the existence of. + * @return true if the object will appear in the output pack file. + */ + public boolean willInclude(final AnyObjectId id) { + return objectsMap.get(id) != null; + } + + /** + * Computes SHA-1 of lexicographically sorted objects ids written in this + * pack, as used to name a pack file in repository. + * + * @return ObjectId representing SHA-1 name of a pack that was created. + */ + public ObjectId computeName() { + final MessageDigest md = Constants.newMessageDigest(); + for (ObjectToPack otp : sortByName()) { + otp.copyRawTo(buf, 0); + md.update(buf, 0, Constants.OBJECT_ID_LENGTH); + } + return ObjectId.fromRaw(md.digest()); + } + + /** + * Create an index file to match the pack file just written. + * <p> + * This method can only be invoked after {@link #preparePack(Iterator)} or + * {@link #preparePack(Collection, Collection)} has been + * invoked and completed successfully. Writing a corresponding index is an + * optional feature that not all pack users may require. + * + * @param indexStream + * output for the index data. Caller is responsible for closing + * this stream. + * @throws IOException + * the index data could not be written to the supplied stream. + */ + public void writeIndex(final OutputStream indexStream) throws IOException { + final List<ObjectToPack> list = sortByName(); + final PackIndexWriter iw; + if (outputVersion <= 0) + iw = PackIndexWriter.createOldestPossible(indexStream, list); + else + iw = PackIndexWriter.createVersion(indexStream, outputVersion); + iw.write(list, packcsum); + } + + private List<ObjectToPack> sortByName() { + if (sortedByName == null) { + sortedByName = new ArrayList<ObjectToPack>(objectsMap.size()); + for (List<ObjectToPack> list : objectsLists) { + for (ObjectToPack otp : list) + sortedByName.add(otp); + } + Collections.sort(sortedByName); + } + return sortedByName; + } + + /** + * Write the prepared pack to the supplied stream. + * <p> + * At first, this method collects and sorts objects to pack, then deltas + * search is performed if set up accordingly, finally pack stream is + * written. {@link ProgressMonitor} tasks {@value #SEARCHING_REUSE_PROGRESS} + * (only if reuseDeltas or reuseObjects is enabled) and + * {@value #WRITING_OBJECTS_PROGRESS} are updated during packing. + * </p> + * <p> + * All reused objects data checksum (Adler32/CRC32) is computed and + * validated against existing checksum. + * </p> + * + * @param packStream + * output stream of pack data. If the stream is not buffered it + * will be buffered by the writer. Caller is responsible for + * closing the stream. + * @throws IOException + * an error occurred reading a local object's data to include in + * the pack, or writing compressed object data to the output + * stream. + */ + public void writePack(OutputStream packStream) throws IOException { + if (reuseDeltas || reuseObjects) + searchForReuse(); + + if (!(packStream instanceof BufferedOutputStream)) + packStream = new BufferedOutputStream(packStream); + out = new PackOutputStream(packStream); + + writeMonitor.beginTask(WRITING_OBJECTS_PROGRESS, getObjectsNumber()); + writeHeader(); + writeObjects(); + writeChecksum(); + + out.flush(); + windowCursor.release(); + writeMonitor.endTask(); + } + + private void searchForReuse() throws IOException { + initMonitor.beginTask(SEARCHING_REUSE_PROGRESS, getObjectsNumber()); + final Collection<PackedObjectLoader> reuseLoaders = new ArrayList<PackedObjectLoader>(); + for (List<ObjectToPack> list : objectsLists) { + for (ObjectToPack otp : list) { + if (initMonitor.isCancelled()) + throw new IOException( + "Packing cancelled during objects writing"); + reuseLoaders.clear(); + searchForReuse(reuseLoaders, otp); + initMonitor.update(1); + } + } + + initMonitor.endTask(); + } + + private void searchForReuse( + final Collection<PackedObjectLoader> reuseLoaders, + final ObjectToPack otp) throws IOException { + db.openObjectInAllPacks(otp, reuseLoaders, windowCursor); + if (reuseDeltas) { + selectDeltaReuseForObject(otp, reuseLoaders); + } + // delta reuse is preferred over object reuse + if (reuseObjects && !otp.hasReuseLoader()) { + selectObjectReuseForObject(otp, reuseLoaders); + } + } + + private void selectDeltaReuseForObject(final ObjectToPack otp, + final Collection<PackedObjectLoader> loaders) throws IOException { + PackedObjectLoader bestLoader = null; + ObjectId bestBase = null; + + for (PackedObjectLoader loader : loaders) { + ObjectId idBase = loader.getDeltaBase(); + if (idBase == null) + continue; + ObjectToPack otpBase = objectsMap.get(idBase); + + // only if base is in set of objects to write or thin-pack's edge + if ((otpBase != null || (thin && edgeObjects.get(idBase) != null)) + // select smallest possible delta if > 1 available + && isBetterDeltaReuseLoader(bestLoader, loader)) { + bestLoader = loader; + bestBase = (otpBase != null ? otpBase : idBase); + } + } + + if (bestLoader != null) { + otp.setReuseLoader(bestLoader); + otp.setDeltaBase(bestBase); + } + } + + private static boolean isBetterDeltaReuseLoader( + PackedObjectLoader currentLoader, PackedObjectLoader loader) + throws IOException { + if (currentLoader == null) + return true; + if (loader.getRawSize() < currentLoader.getRawSize()) + return true; + return (loader.getRawSize() == currentLoader.getRawSize() + && loader.supportsFastCopyRawData() && !currentLoader + .supportsFastCopyRawData()); + } + + private void selectObjectReuseForObject(final ObjectToPack otp, + final Collection<PackedObjectLoader> loaders) { + for (final PackedObjectLoader loader : loaders) { + if (loader instanceof WholePackedObjectLoader) { + otp.setReuseLoader(loader); + return; + } + } + } + + private void writeHeader() throws IOException { + System.arraycopy(Constants.PACK_SIGNATURE, 0, buf, 0, 4); + NB.encodeInt32(buf, 4, PACK_VERSION_GENERATED); + NB.encodeInt32(buf, 8, getObjectsNumber()); + out.write(buf, 0, 12); + } + + private void writeObjects() throws IOException { + for (List<ObjectToPack> list : objectsLists) { + for (ObjectToPack otp : list) { + if (writeMonitor.isCancelled()) + throw new IOException( + "Packing cancelled during objects writing"); + if (!otp.isWritten()) + writeObject(otp); + } + } + } + + private void writeObject(final ObjectToPack otp) throws IOException { + otp.markWantWrite(); + if (otp.isDeltaRepresentation()) { + ObjectToPack deltaBase = otp.getDeltaBase(); + assert deltaBase != null || thin; + if (deltaBase != null && !deltaBase.isWritten()) { + if (deltaBase.wantWrite()) { + otp.clearDeltaBase(); // cycle detected + otp.disposeLoader(); + } else { + writeObject(deltaBase); + } + } + } + + assert !otp.isWritten(); + + out.resetCRC32(); + otp.setOffset(out.length()); + + final PackedObjectLoader reuse = open(otp); + if (reuse != null) { + try { + if (otp.isDeltaRepresentation()) { + writeDeltaObjectReuse(otp, reuse); + } else { + writeObjectHeader(otp.getType(), reuse.getSize()); + reuse.copyRawData(out, buf, windowCursor); + } + } finally { + reuse.endCopyRawData(); + } + } else if (otp.isDeltaRepresentation()) { + throw new IOException("creating deltas is not implemented"); + } else { + writeWholeObjectDeflate(otp); + } + otp.setCRC(out.getCRC32()); + + writeMonitor.update(1); + } + + private PackedObjectLoader open(final ObjectToPack otp) throws IOException { + for (;;) { + PackedObjectLoader reuse = otp.useLoader(); + if (reuse == null) { + return null; + } + + try { + reuse.beginCopyRawData(); + return reuse; + } catch (IOException err) { + // The pack we found the object in originally is gone, or + // it has been overwritten with a different layout. + // + otp.clearDeltaBase(); + searchForReuse(new ArrayList<PackedObjectLoader>(), otp); + continue; + } + } + } + + private void writeWholeObjectDeflate(final ObjectToPack otp) + throws IOException { + final ObjectLoader loader = db.openObject(windowCursor, otp); + final byte[] data = loader.getCachedBytes(); + writeObjectHeader(otp.getType(), data.length); + deflater.reset(); + deflater.setInput(data, 0, data.length); + deflater.finish(); + do { + final int n = deflater.deflate(buf, 0, buf.length); + if (n > 0) + out.write(buf, 0, n); + } while (!deflater.finished()); + } + + private void writeDeltaObjectReuse(final ObjectToPack otp, + final PackedObjectLoader reuse) throws IOException { + if (deltaBaseAsOffset && otp.getDeltaBase() != null) { + writeObjectHeader(Constants.OBJ_OFS_DELTA, reuse.getRawSize()); + + final ObjectToPack deltaBase = otp.getDeltaBase(); + long offsetDiff = otp.getOffset() - deltaBase.getOffset(); + int pos = buf.length - 1; + buf[pos] = (byte) (offsetDiff & 0x7F); + while ((offsetDiff >>= 7) > 0) { + buf[--pos] = (byte) (0x80 | (--offsetDiff & 0x7F)); + } + + out.write(buf, pos, buf.length - pos); + } else { + writeObjectHeader(Constants.OBJ_REF_DELTA, reuse.getRawSize()); + otp.getDeltaBaseId().copyRawTo(buf, 0); + out.write(buf, 0, Constants.OBJECT_ID_LENGTH); + } + reuse.copyRawData(out, buf, windowCursor); + } + + private void writeObjectHeader(final int objectType, long dataLength) + throws IOException { + long nextLength = dataLength >>> 4; + int size = 0; + buf[size++] = (byte) ((nextLength > 0 ? 0x80 : 0x00) + | (objectType << 4) | (dataLength & 0x0F)); + dataLength = nextLength; + while (dataLength > 0) { + nextLength >>>= 7; + buf[size++] = (byte) ((nextLength > 0 ? 0x80 : 0x00) | (dataLength & 0x7F)); + dataLength = nextLength; + } + out.write(buf, 0, size); + } + + private void writeChecksum() throws IOException { + packcsum = out.getDigest(); + out.write(packcsum); + } + + private ObjectWalk setUpWalker( + final Collection<? extends ObjectId> interestingObjects, + final Collection<? extends ObjectId> uninterestingObjects) + throws MissingObjectException, IOException, + IncorrectObjectTypeException { + final ObjectWalk walker = new ObjectWalk(db); + walker.setRetainBody(false); + walker.sort(RevSort.TOPO); + walker.sort(RevSort.COMMIT_TIME_DESC, true); + if (thin) + walker.sort(RevSort.BOUNDARY, true); + + for (ObjectId id : interestingObjects) { + RevObject o = walker.parseAny(id); + walker.markStart(o); + } + if (uninterestingObjects != null) { + for (ObjectId id : uninterestingObjects) { + final RevObject o; + try { + o = walker.parseAny(id); + } catch (MissingObjectException x) { + if (ignoreMissingUninteresting) + continue; + throw x; + } + walker.markUninteresting(o); + } + } + return walker; + } + + private void findObjectsToPack(final ObjectWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + initMonitor.beginTask(COUNTING_OBJECTS_PROGRESS, + ProgressMonitor.UNKNOWN); + RevObject o; + + while ((o = walker.next()) != null) { + addObject(o); + initMonitor.update(1); + } + while ((o = walker.nextObject()) != null) { + addObject(o); + initMonitor.update(1); + } + initMonitor.endTask(); + } + + /** + * Include one object to the output file. + * <p> + * Objects are written in the order they are added. If the same object is + * added twice, it may be written twice, creating a larger than necessary + * file. + * + * @param object + * the object to add. + * @throws IncorrectObjectTypeException + * the object is an unsupported type. + */ + public void addObject(final RevObject object) + throws IncorrectObjectTypeException { + if (object.has(RevFlag.UNINTERESTING)) { + edgeObjects.add(object); + thin = true; + return; + } + + final ObjectToPack otp = new ObjectToPack(object, object.getType()); + try { + objectsLists[object.getType()].add(otp); + } catch (ArrayIndexOutOfBoundsException x) { + throw new IncorrectObjectTypeException(object, + "COMMIT nor TREE nor BLOB nor TAG"); + } catch (UnsupportedOperationException x) { + // index pointing to "dummy" empty list + throw new IncorrectObjectTypeException(object, + "COMMIT nor TREE nor BLOB nor TAG"); + } + objectsMap.add(otp); + } + + /** + * Class holding information about object that is going to be packed by + * {@link PackWriter}. Information include object representation in a + * pack-file and object status. + * + */ + static class ObjectToPack extends PackedObjectInfo { + private ObjectId deltaBase; + + private PackedObjectLoader reuseLoader; + + /** + * Bit field, from bit 0 to bit 31: + * <ul> + * <li>1 bit: wantWrite</li> + * <li>3 bits: type</li> + * <li>28 bits: deltaDepth</li> + * </ul> + */ + private int flags; + + /** + * Construct object for specified object id. <br/> By default object is + * marked as not written and non-delta packed (as a whole object). + * + * @param src + * object id of object for packing + * @param type + * real type code of the object, not its in-pack type. + */ + ObjectToPack(AnyObjectId src, final int type) { + super(src); + flags |= type << 1; + } + + /** + * @return delta base object id if object is going to be packed in delta + * representation; null otherwise - if going to be packed as a + * whole object. + */ + ObjectId getDeltaBaseId() { + return deltaBase; + } + + /** + * @return delta base object to pack if object is going to be packed in + * delta representation and delta is specified as object to + * pack; null otherwise - if going to be packed as a whole + * object or delta base is specified only as id. + */ + ObjectToPack getDeltaBase() { + if (deltaBase instanceof ObjectToPack) + return (ObjectToPack) deltaBase; + return null; + } + + /** + * Set delta base for the object. Delta base set by this method is used + * by {@link PackWriter} to write object - determines its representation + * in a created pack. + * + * @param deltaBase + * delta base object or null if object should be packed as a + * whole object. + * + */ + void setDeltaBase(ObjectId deltaBase) { + this.deltaBase = deltaBase; + } + + void clearDeltaBase() { + this.deltaBase = null; + } + + /** + * @return true if object is going to be written as delta; false + * otherwise. + */ + boolean isDeltaRepresentation() { + return deltaBase != null; + } + + /** + * Check if object is already written in a pack. This information is + * used to achieve delta-base precedence in a pack file. + * + * @return true if object is already written; false otherwise. + */ + boolean isWritten() { + return getOffset() != 0; + } + + PackedObjectLoader useLoader() { + final PackedObjectLoader r = reuseLoader; + reuseLoader = null; + return r; + } + + boolean hasReuseLoader() { + return reuseLoader != null; + } + + void setReuseLoader(PackedObjectLoader reuseLoader) { + this.reuseLoader = reuseLoader; + } + + void disposeLoader() { + this.reuseLoader = null; + } + + int getType() { + return (flags>>1) & 0x7; + } + + int getDeltaDepth() { + return flags >>> 4; + } + + void updateDeltaDepth() { + final int d; + if (deltaBase instanceof ObjectToPack) + d = ((ObjectToPack) deltaBase).getDeltaDepth() + 1; + else if (deltaBase != null) + d = 1; + else + d = 0; + flags = (d << 4) | flags & 0x15; + } + + boolean wantWrite() { + return (flags & 1) == 1; + } + + void markWantWrite() { + flags |= 1; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackedObjectLoader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackedObjectLoader.java new file mode 100644 index 0000000000..4125579b22 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PackedObjectLoader.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Base class for a set of object loader classes for packed objects. + */ +abstract class PackedObjectLoader extends ObjectLoader { + protected final PackFile pack; + + protected final long dataOffset; + + protected final long objectOffset; + + protected int objectType; + + protected int objectSize; + + protected byte[] cachedBytes; + + PackedObjectLoader(final PackFile pr, final long dataOffset, + final long objectOffset) { + pack = pr; + this.dataOffset = dataOffset; + this.objectOffset = objectOffset; + } + + /** + * Force this object to be loaded into memory and pinned in this loader. + * <p> + * Once materialized, subsequent get operations for the following methods + * will always succeed without raising an exception, as all information is + * pinned in memory by this loader instance. + * <ul> + * <li>{@link #getType()}</li> + * <li>{@link #getSize()}</li> + * <li>{@link #getBytes()}, {@link #getCachedBytes}</li> + * <li>{@link #getRawSize()}</li> + * <li>{@link #getRawType()}</li> + * </ul> + * + * @param curs + * temporary thread storage during data access. + * @throws IOException + * the object cannot be read. + */ + public abstract void materialize(WindowCursor curs) throws IOException; + + public final int getType() { + return objectType; + } + + public final long getSize() { + return objectSize; + } + + @Override + public final byte[] getCachedBytes() { + return cachedBytes; + } + + /** + * @return offset of object header within pack file + */ + public final long getObjectOffset() { + return objectOffset; + } + + /** + * @return offset of object data within pack file + */ + public final long getDataOffset() { + return dataOffset; + } + + /** + * Peg the pack file open to support data copying. + * <p> + * Applications trying to copy raw pack data should ensure the pack stays + * open and available throughout the entire copy. To do that use: + * + * <pre> + * loader.beginCopyRawData(); + * try { + * loader.copyRawData(out, tmpbuf, curs); + * } finally { + * loader.endCopyRawData(); + * } + * </pre> + * + * @throws IOException + * this loader contains stale information and cannot be used. + * The most likely cause is the underlying pack file has been + * deleted, and the object has moved to another pack file. + */ + public void beginCopyRawData() throws IOException { + pack.beginCopyRawData(); + } + + /** + * Copy raw object representation from storage to provided output stream. + * <p> + * Copied data doesn't include object header. User must provide temporary + * buffer used during copying by underlying I/O layer. + * </p> + * + * @param out + * output stream when data is copied. No buffering is guaranteed. + * @param buf + * temporary buffer used during copying. Recommended size is at + * least few kB. + * @param curs + * temporary thread storage during data access. + * @throws IOException + * when the object cannot be read. + * @see #beginCopyRawData() + */ + public void copyRawData(OutputStream out, byte buf[], WindowCursor curs) + throws IOException { + pack.copyRawData(this, out, buf, curs); + } + + /** Release resources after {@link #beginCopyRawData()}. */ + public void endCopyRawData() { + pack.endCopyRawData(); + } + + /** + * @return true if this loader is capable of fast raw-data copying basing on + * compressed data checksum; false if raw-data copying needs + * uncompressing and compressing data + * @throws IOException + * the index file format cannot be determined. + */ + public boolean supportsFastCopyRawData() throws IOException { + return pack.supportsFastCopyRawData(); + } + + /** + * @return id of delta base object for this object representation. null if + * object is not stored as delta. + * @throws IOException + * when delta base cannot read. + */ + public abstract ObjectId getDeltaBase() throws IOException; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java new file mode 100644 index 0000000000..a9f520e8fc --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import org.eclipse.jgit.util.SystemReader; + +/** + * A combination of a person identity and time in Git. + * + * Git combines Name + email + time + time zone to specify who wrote or + * committed something. + */ +public class PersonIdent { + private final String name; + + private final String emailAddress; + + private final long when; + + private final int tzOffset; + + /** + * Creates new PersonIdent from config info in repository, with current time. + * This new PersonIdent gets the info from the default committer as available + * from the configuration. + * + * @param repo + */ + public PersonIdent(final Repository repo) { + final RepositoryConfig config = repo.getConfig(); + name = config.getCommitterName(); + emailAddress = config.getCommitterEmail(); + when = SystemReader.getInstance().getCurrentTime(); + tzOffset = SystemReader.getInstance().getTimezone(when); + } + + /** + * Copy a {@link PersonIdent}. + * + * @param pi + * Original {@link PersonIdent} + */ + public PersonIdent(final PersonIdent pi) { + this(pi.getName(), pi.getEmailAddress()); + } + + /** + * Construct a new {@link PersonIdent} with current time. + * + * @param aName + * @param aEmailAddress + */ + public PersonIdent(final String aName, final String aEmailAddress) { + this(aName, aEmailAddress, new Date(), TimeZone.getDefault()); + } + + /** + * Copy a PersonIdent, but alter the clone's time stamp + * + * @param pi + * original {@link PersonIdent} + * @param when + * local time + * @param tz + * time zone + */ + public PersonIdent(final PersonIdent pi, final Date when, final TimeZone tz) { + this(pi.getName(), pi.getEmailAddress(), when, tz); + } + + /** + * Copy a {@link PersonIdent}, but alter the clone's time stamp + * + * @param pi + * original {@link PersonIdent} + * @param aWhen + * local time + */ + public PersonIdent(final PersonIdent pi, final Date aWhen) { + name = pi.getName(); + emailAddress = pi.getEmailAddress(); + when = aWhen.getTime(); + tzOffset = pi.tzOffset; + } + + /** + * Construct a PersonIdent from simple data + * + * @param aName + * @param aEmailAddress + * @param aWhen + * local time stamp + * @param aTZ + * time zone + */ + public PersonIdent(final String aName, final String aEmailAddress, + final Date aWhen, final TimeZone aTZ) { + name = aName; + emailAddress = aEmailAddress; + when = aWhen.getTime(); + tzOffset = aTZ.getOffset(when) / (60 * 1000); + } + + /** + * Construct a {@link PersonIdent} + * + * @param aName + * @param aEmailAddress + * @param aWhen + * local time stamp + * @param aTZ + * time zone + */ + public PersonIdent(final String aName, final String aEmailAddress, + final long aWhen, final int aTZ) { + name = aName; + emailAddress = aEmailAddress; + when = aWhen; + tzOffset = aTZ; + } + + /** + * Copy a PersonIdent, but alter the clone's time stamp + * + * @param pi + * original {@link PersonIdent} + * @param aWhen + * local time stamp + * @param aTZ + * time zone + */ + public PersonIdent(final PersonIdent pi, final long aWhen, final int aTZ) { + name = pi.getName(); + emailAddress = pi.getEmailAddress(); + when = aWhen; + tzOffset = aTZ; + } + + /** + * Construct a PersonIdent from a string with full name, email, time time + * zone string. The input string must be valid. + * + * @param in + * a Git internal format author/committer string. + */ + public PersonIdent(final String in) { + final int lt = in.indexOf('<'); + if (lt == -1) { + throw new IllegalArgumentException("Malformed PersonIdent string" + + " (no < was found): " + in); + } + final int gt = in.indexOf('>', lt); + if (gt == -1) { + throw new IllegalArgumentException("Malformed PersonIdent string" + + " (no > was found): " + in); + } + final int sp = in.indexOf(' ', gt + 2); + if (sp == -1) { + when = 0; + tzOffset = -1; + } else { + final String tzHoursStr = in.substring(sp + 1, sp + 4).trim(); + final int tzHours; + if (tzHoursStr.charAt(0) == '+') { + tzHours = Integer.parseInt(tzHoursStr.substring(1)); + } else { + tzHours = Integer.parseInt(tzHoursStr); + } + final int tzMins = Integer.parseInt(in.substring(sp + 4).trim()); + when = Long.parseLong(in.substring(gt + 1, sp).trim()) * 1000; + tzOffset = tzHours * 60 + tzMins; + } + + name = in.substring(0, lt).trim(); + emailAddress = in.substring(lt + 1, gt).trim(); + } + + /** + * @return Name of person + */ + public String getName() { + return name; + } + + /** + * @return email address of person + */ + public String getEmailAddress() { + return emailAddress; + } + + /** + * @return timestamp + */ + public Date getWhen() { + return new Date(when); + } + + /** + * @return this person's declared time zone; null if time zone is unknown. + */ + public TimeZone getTimeZone() { + StringBuffer tzId = new StringBuffer(8); + tzId.append("GMT"); + appendTimezone(tzId); + return TimeZone.getTimeZone(tzId.toString()); + } + + /** + * @return this person's declared time zone as minutes east of UTC. If the + * timezone is to the west of UTC it is negative. + */ + public int getTimeZoneOffset() { + return tzOffset; + } + + public int hashCode() { + return getEmailAddress().hashCode() ^ (int) when; + } + + public boolean equals(final Object o) { + if (o instanceof PersonIdent) { + final PersonIdent p = (PersonIdent) o; + return getName().equals(p.getName()) + && getEmailAddress().equals(p.getEmailAddress()) + && when == p.when; + } + return false; + } + + /** + * Format for Git storage. + * + * @return a string in the git author format + */ + public String toExternalString() { + final StringBuffer r = new StringBuffer(); + r.append(getName()); + r.append(" <"); + r.append(getEmailAddress()); + r.append("> "); + r.append(when / 1000); + r.append(' '); + appendTimezone(r); + return r.toString(); + } + + private void appendTimezone(final StringBuffer r) { + int offset = tzOffset; + final char sign; + final int offsetHours; + final int offsetMins; + + if (offset < 0) { + sign = '-'; + offset = -offset; + } else { + sign = '+'; + } + + offsetHours = offset / 60; + offsetMins = offset % 60; + + r.append(sign); + if (offsetHours < 10) { + r.append('0'); + } + r.append(offsetHours); + if (offsetMins < 10) { + r.append('0'); + } + r.append(offsetMins); + } + + public String toString() { + final StringBuffer r = new StringBuffer(); + final SimpleDateFormat dtfmt; + dtfmt = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy Z", Locale.US); + dtfmt.setTimeZone(getTimeZone()); + + r.append("PersonIdent["); + r.append(getName()); + r.append(", "); + r.append(getEmailAddress()); + r.append(", "); + r.append(dtfmt.format(Long.valueOf(when))); + r.append("]"); + + return r.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ProgressMonitor.java new file mode 100644 index 0000000000..cab1b08584 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ProgressMonitor.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +/** A progress reporting interface. */ +public interface ProgressMonitor { + /** Constant indicating the total work units cannot be predicted. */ + public static final int UNKNOWN = 0; + + /** + * Advise the monitor of the total number of subtasks. + * <p> + * This should be invoked at most once per progress monitor interface. + * + * @param totalTasks + * the total number of tasks the caller will need to complete + * their processing. + */ + void start(int totalTasks); + + /** + * Begin processing a single task. + * + * @param title + * title to describe the task. Callers should publish these as + * stable string constants that implementations could match + * against for translation support. + * @param totalWork + * total number of work units the application will perform; + * {@link #UNKNOWN} if it cannot be predicted in advance. + */ + void beginTask(String title, int totalWork); + + /** + * Denote that some work units have been completed. + * <p> + * This is an incremental update; if invoked once per work unit the correct + * value for our argument is <code>1</code>, to indicate a single unit of + * work has been finished by the caller. + * + * @param completed + * the number of work units completed since the last call. + */ + void update(int completed); + + /** Finish the current task, so the next can begin. */ + void endTask(); + + /** + * Check for user task cancellation. + * + * @return true if the user asked the process to stop working. + */ + boolean isCancelled(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java new file mode 100644 index 0000000000..b040e9bedd --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +/** + * Pairing of a name and the {@link ObjectId} it currently has. + * <p> + * A ref in Git is (more or less) a variable that holds a single object + * identifier. The object identifier can be any valid Git object (blob, tree, + * commit, annotated tag, ...). + * <p> + * The ref name has the attributes of the ref that was asked for as well as + * the ref it was resolved to for symbolic refs plus the object id it points + * to and (for tags) the peeled target object id, i.e. the tag resolved + * recursively until a non-tag object is referenced. + */ +public class Ref { + /** Location where a {@link Ref} is stored. */ + public static enum Storage { + /** + * The ref does not exist yet, updating it may create it. + * <p> + * Creation is likely to choose {@link #LOOSE} storage. + */ + NEW(true, false), + + /** + * The ref is stored in a file by itself. + * <p> + * Updating this ref affects only this ref. + */ + LOOSE(true, false), + + /** + * The ref is stored in the <code>packed-refs</code> file, with + * others. + * <p> + * Updating this ref requires rewriting the file, with perhaps many + * other refs being included at the same time. + */ + PACKED(false, true), + + /** + * The ref is both {@link #LOOSE} and {@link #PACKED}. + * <p> + * Updating this ref requires only updating the loose file, but deletion + * requires updating both the loose file and the packed refs file. + */ + LOOSE_PACKED(true, true), + + /** + * The ref came from a network advertisement and storage is unknown. + * <p> + * This ref cannot be updated without Git-aware support on the remote + * side, as Git-aware code consolidate the remote refs and reported them + * to this process. + */ + NETWORK(false, false); + + private final boolean loose; + + private final boolean packed; + + private Storage(final boolean l, final boolean p) { + loose = l; + packed = p; + } + + /** + * @return true if this storage has a loose file. + */ + public boolean isLoose() { + return loose; + } + + /** + * @return true if this storage is inside the packed file. + */ + public boolean isPacked() { + return packed; + } + } + + private final Storage storage; + + private final String name; + + private ObjectId objectId; + + private ObjectId peeledObjectId; + + private final String origName; + + private final boolean peeled; + + /** + * Create a new ref pairing. + * + * @param st + * method used to store this ref. + * @param origName + * The name used to resolve this ref + * @param refName + * name of this ref. + * @param id + * current value of the ref. May be null to indicate a ref that + * does not exist yet. + */ + public Ref(final Storage st, final String origName, final String refName, final ObjectId id) { + this(st, origName, refName, id, null, false); + } + + /** + * Create a new ref pairing. + * + * @param st + * method used to store this ref. + * @param refName + * name of this ref. + * @param id + * current value of the ref. May be null to indicate a ref that + * does not exist yet. + */ + public Ref(final Storage st, final String refName, final ObjectId id) { + this(st, refName, refName, id, null, false); + } + + /** + * Create a new ref pairing. + * + * @param st + * method used to store this ref. + * @param origName + * The name used to resolve this ref + * @param refName + * name of this ref. + * @param id + * current value of the ref. May be null to indicate a ref that + * does not exist yet. + * @param peel + * peeled value of the ref's tag. May be null if this is not a + * tag or not yet peeled (in which case the next parameter should be null) + * @param peeled + * true if peel represents a the peeled value of the object + */ + public Ref(final Storage st, final String origName, final String refName, final ObjectId id, + final ObjectId peel, final boolean peeled) { + storage = st; + this.origName = origName; + name = refName; + objectId = id; + peeledObjectId = peel; + this.peeled = peeled; + } + + /** + * Create a new ref pairing. + * + * @param st + * method used to store this ref. + * @param refName + * name of this ref. + * @param id + * current value of the ref. May be null to indicate a ref that + * does not exist yet. + * @param peel + * peeled value of the ref's tag. May be null if this is not a + * tag or the peeled value is not known. + * @param peeled + * true if peel represents a the peeled value of the object + */ + public Ref(final Storage st, final String refName, final ObjectId id, + final ObjectId peel, boolean peeled) { + this(st, refName, refName, id, peel, peeled); + } + + /** + * What this ref is called within the repository. + * + * @return name of this ref. + */ + public String getName() { + return name; + } + + /** + * @return the originally resolved name + */ + public String getOrigName() { + return origName; + } + + /** + * Cached value of this ref. + * + * @return the value of this ref at the last time we read it. + */ + public ObjectId getObjectId() { + return objectId; + } + + /** + * Cached value of <code>ref^{}</code> (the ref peeled to commit). + * + * @return if this ref is an annotated tag the id of the commit (or tree or + * blob) that the annotated tag refers to; null if this ref does not + * refer to an annotated tag. + */ + public ObjectId getPeeledObjectId() { + if (!peeled) + return null; + return peeledObjectId; + } + + /** + * @return whether the Ref represents a peeled tag + */ + public boolean isPeeled() { + return peeled; + } + + /** + * How was this ref obtained? + * <p> + * The current storage model of a Ref may influence how the ref must be + * updated or deleted from the repository. + * + * @return type of ref. + */ + public Storage getStorage() { + return storage; + } + + public String toString() { + String o = ""; + if (!origName.equals(name)) + o = "(" + origName + ")"; + return "Ref[" + o + name + "=" + ObjectId.toString(getObjectId()) + "]"; + } + + void setPeeledObjectId(final ObjectId id) { + peeledObjectId = id; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java new file mode 100644 index 0000000000..cbbc0a91cc --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefComparator.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2008, Charles O'Farrell <charleso@charleso.org> + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Util for sorting (or comparing) Ref instances by name. + * <p> + * Useful for command line tools or writing out refs to file. + */ +public class RefComparator implements Comparator<Ref> { + + /** Singleton instance of RefComparator */ + public static final RefComparator INSTANCE = new RefComparator(); + + public int compare(final Ref o1, final Ref o2) { + return o1.getOrigName().compareTo(o2.getOrigName()); + } + + /** + * Sorts the collection of refs, returning a new collection. + * + * @param refs + * collection to be sorted + * @return sorted collection of refs + */ + public static Collection<Ref> sort(final Collection<Ref> refs) { + final List<Ref> r = new ArrayList<Ref>(refs); + Collections.sort(r, INSTANCE); + return r; + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java new file mode 100644 index 0000000000..2c68dbb6d4 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java @@ -0,0 +1,522 @@ +/* + * Copyright (C) 2007-2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import static org.eclipse.jgit.lib.Constants.R_TAGS; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jgit.errors.ObjectWritingException; +import org.eclipse.jgit.lib.Ref.Storage; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.RawParseUtils; + +class RefDatabase { + private static final String REFS_SLASH = "refs/"; + + private static final String[] refSearchPaths = { "", REFS_SLASH, + R_TAGS, Constants.R_HEADS, Constants.R_REMOTES }; + + private final Repository db; + + private final File gitDir; + + private final File refsDir; + + private Map<String, Ref> looseRefs; + private Map<String, Long> looseRefsMTime; + private Map<String, String> looseSymRefs; + + private final File packedRefsFile; + + private Map<String, Ref> packedRefs; + + private long packedRefsLastModified; + + private long packedRefsLength; + + int lastRefModification; + + int lastNotifiedRefModification; + + private int refModificationCounter; + + RefDatabase(final Repository r) { + db = r; + gitDir = db.getDirectory(); + refsDir = FS.resolve(gitDir, "refs"); + packedRefsFile = FS.resolve(gitDir, Constants.PACKED_REFS); + clearCache(); + } + + synchronized void clearCache() { + looseRefs = new HashMap<String, Ref>(); + looseRefsMTime = new HashMap<String, Long>(); + packedRefs = new HashMap<String, Ref>(); + looseSymRefs = new HashMap<String, String>(); + packedRefsLastModified = 0; + packedRefsLength = 0; + } + + Repository getRepository() { + return db; + } + + void create() { + refsDir.mkdir(); + new File(refsDir, "heads").mkdir(); + new File(refsDir, "tags").mkdir(); + } + + ObjectId idOf(final String name) throws IOException { + refreshPackedRefs(); + final Ref r = readRefBasic(name, 0); + return r != null ? r.getObjectId() : null; + } + + /** + * Create a command to update, create or delete a ref in this repository. + * + * @param name + * name of the ref the caller wants to modify. + * @return an update command. The caller must finish populating this command + * and then invoke one of the update methods to actually make a + * change. + * @throws IOException + * a symbolic ref was passed in and could not be resolved back + * to the base ref, as the symbolic ref could not be read. + */ + RefUpdate newUpdate(final String name) throws IOException { + refreshPackedRefs(); + Ref r = readRefBasic(name, 0); + if (r == null) + r = new Ref(Ref.Storage.NEW, name, null); + return new RefUpdate(this, r, fileForRef(r.getName())); + } + + void stored(final String origName, final String name, final ObjectId id, final long time) { + synchronized (this) { + looseRefs.put(name, new Ref(Ref.Storage.LOOSE, name, name, id)); + looseRefsMTime.put(name, time); + setModified(); + } + db.fireRefsMaybeChanged(); + } + + /** + * An set of update operations for renaming a ref + * + * @param fromRef Old ref name + * @param toRef New ref name + * @return a RefUpdate operation to rename a ref + * @throws IOException + */ + RefRename newRename(String fromRef, String toRef) throws IOException { + refreshPackedRefs(); + Ref f = readRefBasic(fromRef, 0); + Ref t = new Ref(Ref.Storage.NEW, toRef, null); + RefUpdate refUpdateFrom = new RefUpdate(this, f, fileForRef(f.getName())); + RefUpdate refUpdateTo = new RefUpdate(this, t, fileForRef(t.getName())); + return new RefRename(refUpdateTo, refUpdateFrom); + } + + /** + * Writes a symref (e.g. HEAD) to disk + * + * @param name + * symref name + * @param target + * pointed to ref + * @throws IOException + */ + void link(final String name, final String target) throws IOException { + final byte[] content = Constants.encode("ref: " + target + "\n"); + lockAndWriteFile(fileForRef(name), content); + synchronized (this) { + looseSymRefs.remove(name); + setModified(); + } + db.fireRefsMaybeChanged(); + } + + void uncacheSymRef(String name) { + synchronized(this) { + looseSymRefs.remove(name); + setModified(); + } + } + + void uncacheRef(String name) { + looseRefs.remove(name); + looseRefsMTime.remove(name); + packedRefs.remove(name); + } + + private void setModified() { + lastRefModification = refModificationCounter++; + } + + Ref readRef(final String partialName) throws IOException { + refreshPackedRefs(); + for (int k = 0; k < refSearchPaths.length; k++) { + final Ref r = readRefBasic(refSearchPaths[k] + partialName, 0); + if (r != null && r.getObjectId() != null) + return r; + } + return null; + } + + /** + * @return all known refs (heads, tags, remotes). + */ + Map<String, Ref> getAllRefs() { + return readRefs(); + } + + /** + * @return all tags; key is short tag name ("v1.0") and value of the entry + * contains the ref with the full tag name ("refs/tags/v1.0"). + */ + Map<String, Ref> getTags() { + final Map<String, Ref> tags = new HashMap<String, Ref>(); + for (final Ref r : readRefs().values()) { + if (r.getName().startsWith(R_TAGS)) + tags.put(r.getName().substring(R_TAGS.length()), r); + } + return tags; + } + + private Map<String, Ref> readRefs() { + final HashMap<String, Ref> avail = new HashMap<String, Ref>(); + readPackedRefs(avail); + readLooseRefs(avail, REFS_SLASH, refsDir); + try { + final Ref r = readRefBasic(Constants.HEAD, 0); + if (r != null && r.getObjectId() != null) + avail.put(Constants.HEAD, r); + } catch (IOException e) { + // ignore here + } + db.fireRefsMaybeChanged(); + return avail; + } + + private synchronized void readPackedRefs(final Map<String, Ref> avail) { + refreshPackedRefs(); + avail.putAll(packedRefs); + } + + private void readLooseRefs(final Map<String, Ref> avail, + final String prefix, final File dir) { + final File[] entries = dir.listFiles(); + if (entries == null) + return; + + for (final File ent : entries) { + final String entName = ent.getName(); + if (".".equals(entName) || "..".equals(entName)) + continue; + if (ent.isDirectory()) { + readLooseRefs(avail, prefix + entName + "/", ent); + } else { + try { + final Ref ref = readRefBasic(prefix + entName, 0); + if (ref != null) + avail.put(ref.getOrigName(), ref); + } catch (IOException e) { + continue; + } + } + } + } + + Ref peel(final Ref ref) { + if (ref.isPeeled()) + return ref; + ObjectId peeled = null; + try { + Object target = db.mapObject(ref.getObjectId(), ref.getName()); + while (target instanceof Tag) { + final Tag tag = (Tag)target; + peeled = tag.getObjId(); + if (Constants.TYPE_TAG.equals(tag.getType())) + target = db.mapObject(tag.getObjId(), ref.getName()); + else + break; + } + } catch (IOException e) { + // Ignore a read error. Â Callers will also get the same error + // if they try to use the result of getPeeledObjectId. + } + return new Ref(ref.getStorage(), ref.getName(), ref.getObjectId(), peeled, true); + + } + + private File fileForRef(final String name) { + if (name.startsWith(REFS_SLASH)) + return new File(refsDir, name.substring(REFS_SLASH.length())); + return new File(gitDir, name); + } + + private Ref readRefBasic(final String name, final int depth) throws IOException { + return readRefBasic(name, name, depth); + } + + private synchronized Ref readRefBasic(final String origName, + final String name, final int depth) throws IOException { + // Prefer loose ref to packed ref as the loose + // file can be more up-to-date than a packed one. + // + Ref ref = looseRefs.get(origName); + final File loose = fileForRef(name); + final long mtime = loose.lastModified(); + if (ref != null) { + Long cachedlastModified = looseRefsMTime.get(name); + if (cachedlastModified != null && cachedlastModified == mtime) { + if (packedRefs.containsKey(origName)) + return new Ref(Storage.LOOSE_PACKED, origName, ref + .getObjectId(), ref.getPeeledObjectId(), ref + .isPeeled()); + else + return ref; + } + looseRefs.remove(origName); + looseRefsMTime.remove(origName); + } + + if (mtime == 0) { + // If last modified is 0 the file does not exist. + // Try packed cache. + // + ref = packedRefs.get(name); + if (ref != null) + if (!ref.getOrigName().equals(origName)) + ref = new Ref(Storage.LOOSE_PACKED, origName, name, ref.getObjectId()); + return ref; + } + + String line = null; + try { + Long cachedlastModified = looseRefsMTime.get(name); + if (cachedlastModified != null && cachedlastModified == mtime) { + line = looseSymRefs.get(name); + } + if (line == null) { + line = readLine(loose); + looseRefsMTime.put(name, mtime); + looseSymRefs.put(name, line); + } + } catch (FileNotFoundException notLoose) { + return packedRefs.get(name); + } + + if (line == null || line.length() == 0) { + looseRefs.remove(origName); + looseRefsMTime.remove(origName); + return new Ref(Ref.Storage.LOOSE, origName, name, null); + } + + if (line.startsWith("ref: ")) { + if (depth >= 5) { + throw new IOException("Exceeded maximum ref depth of " + depth + + " at " + name + ". Circular reference?"); + } + + final String target = line.substring("ref: ".length()); + Ref r = readRefBasic(target, target, depth + 1); + Long cachedMtime = looseRefsMTime.get(name); + if (cachedMtime != null && cachedMtime != mtime) + setModified(); + looseRefsMTime.put(name, mtime); + if (r == null) + return new Ref(Ref.Storage.LOOSE, origName, target, null); + if (!origName.equals(r.getName())) + r = new Ref(Ref.Storage.LOOSE_PACKED, origName, r.getName(), r.getObjectId(), r.getPeeledObjectId(), true); + return r; + } + + setModified(); + + final ObjectId id; + try { + id = ObjectId.fromString(line); + } catch (IllegalArgumentException notRef) { + throw new IOException("Not a ref: " + name + ": " + line); + } + + Storage storage; + if (packedRefs.containsKey(name)) + storage = Ref.Storage.LOOSE_PACKED; + else + storage = Ref.Storage.LOOSE; + ref = new Ref(storage, name, id); + looseRefs.put(name, ref); + looseRefsMTime.put(name, mtime); + + if (!origName.equals(name)) { + ref = new Ref(Ref.Storage.LOOSE, origName, name, id); + looseRefs.put(origName, ref); + } + + return ref; + } + + private synchronized void refreshPackedRefs() { + final long currTime = packedRefsFile.lastModified(); + final long currLen = currTime == 0 ? 0 : packedRefsFile.length(); + if (currTime == packedRefsLastModified && currLen == packedRefsLength) + return; + if (currTime == 0) { + packedRefsLastModified = 0; + packedRefsLength = 0; + packedRefs = new HashMap<String, Ref>(); + return; + } + + final Map<String, Ref> newPackedRefs = new HashMap<String, Ref>(); + try { + final BufferedReader b = openReader(packedRefsFile); + try { + String p; + Ref last = null; + while ((p = b.readLine()) != null) { + if (p.charAt(0) == '#') + continue; + + if (p.charAt(0) == '^') { + if (last == null) + throw new IOException("Peeled line before ref."); + + final ObjectId id = ObjectId.fromString(p.substring(1)); + last = new Ref(Ref.Storage.PACKED, last.getName(), last + .getName(), last.getObjectId(), id, true); + newPackedRefs.put(last.getName(), last); + continue; + } + + final int sp = p.indexOf(' '); + final ObjectId id = ObjectId.fromString(p.substring(0, sp)); + final String name = copy(p, sp + 1, p.length()); + last = new Ref(Ref.Storage.PACKED, name, name, id); + newPackedRefs.put(last.getName(), last); + } + } finally { + b.close(); + } + packedRefsLastModified = currTime; + packedRefsLength = currLen; + packedRefs = newPackedRefs; + setModified(); + } catch (FileNotFoundException noPackedRefs) { + // Ignore it and leave the new map empty. + // + packedRefsLastModified = 0; + packedRefsLength = 0; + packedRefs = newPackedRefs; + } catch (IOException e) { + throw new RuntimeException("Cannot read packed refs", e); + } + } + + private static String copy(final String src, final int off, final int end) { + return new StringBuilder(end - off).append(src, off, end).toString(); + } + + private void lockAndWriteFile(File file, byte[] content) throws IOException { + String name = file.getName(); + final LockFile lck = new LockFile(file); + if (!lck.lock()) + throw new ObjectWritingException("Unable to lock " + name); + try { + lck.write(content); + } catch (IOException ioe) { + throw new ObjectWritingException("Unable to write " + name, ioe); + } + if (!lck.commit()) + throw new ObjectWritingException("Unable to write " + name); + } + + synchronized void removePackedRef(String name) throws IOException { + packedRefs.remove(name); + writePackedRefs(); + } + + private void writePackedRefs() throws IOException { + new RefWriter(packedRefs.values()) { + @Override + protected void writeFile(String name, byte[] content) throws IOException { + lockAndWriteFile(new File(db.getDirectory(), name), content); + } + }.writePackedRefs(); + } + + private static String readLine(final File file) + throws FileNotFoundException, IOException { + final byte[] buf = NB.readFully(file, 4096); + int n = buf.length; + + // remove trailing whitespaces + while (n > 0 && Character.isWhitespace(buf[n - 1])) + n--; + + if (n == 0) + return null; + return RawParseUtils.decode(buf, 0, n); + } + + private static BufferedReader openReader(final File fileLocation) + throws FileNotFoundException { + return new BufferedReader(new InputStreamReader(new FileInputStream( + fileLocation), Constants.CHARSET)); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefLogWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefLogWriter.java new file mode 100644 index 0000000000..d41bbb644b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefLogWriter.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2007-2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Utility class to work with reflog files + * + * @author Dave Watson + */ +public class RefLogWriter { + static void append(final RefUpdate u, final String msg) throws IOException { + final ObjectId oldId = u.getOldObjectId(); + final ObjectId newId = u.getNewObjectId(); + final Repository db = u.getRepository(); + final PersonIdent ident = u.getRefLogIdent(); + + appendOneRecord(oldId, newId, ident, msg, db, u.getName()); + if (!u.getName().equals(u.getOrigName())) + appendOneRecord(oldId, newId, ident, msg, db, u.getOrigName()); + } + + static void append(RefRename refRename, String logName, String msg) throws IOException { + final ObjectId id = refRename.getObjectId(); + final Repository db = refRename.getRepository(); + final PersonIdent ident = refRename.getRefLogIdent(); + appendOneRecord(id, id, ident, msg, db, logName); + } + + static void renameTo(final Repository db, final RefUpdate from, + final RefUpdate to) throws IOException { + final File logdir = new File(db.getDirectory(), Constants.LOGS); + final File reflogFrom = new File(logdir, from.getName()); + if (!reflogFrom.exists()) + return; + final File reflogTo = new File(logdir, to.getName()); + final File reflogToDir = reflogTo.getParentFile(); + File tmp = new File(logdir, "tmp-renamed-log.." + Thread.currentThread().getId()); + if (!reflogFrom.renameTo(tmp)) { + throw new IOException("Cannot rename " + reflogFrom + " to (" + tmp + + ")" + reflogTo); + } + RefUpdate.deleteEmptyDir(reflogFrom, RefUpdate.count(from.getName(), + '/')); + if (!reflogToDir.exists() && !reflogToDir.mkdirs()) { + throw new IOException("Cannot create directory " + reflogToDir); + } + if (!tmp.renameTo(reflogTo)) { + throw new IOException("Cannot rename (" + tmp + ")" + reflogFrom + + " to " + reflogTo); + } + } + + private static void appendOneRecord(final ObjectId oldId, + final ObjectId newId, PersonIdent ident, final String msg, + final Repository db, final String refName) throws IOException { + if (ident == null) + ident = new PersonIdent(db); + else + ident = new PersonIdent(ident); + + final StringBuilder r = new StringBuilder(); + r.append(ObjectId.toString(oldId)); + r.append(' '); + r.append(ObjectId.toString(newId)); + r.append(' '); + r.append(ident.toExternalString()); + r.append('\t'); + r.append(msg); + r.append('\n'); + + final byte[] rec = Constants.encode(r.toString()); + final File logdir = new File(db.getDirectory(), Constants.LOGS); + final File reflog = new File(logdir, refName); + final File refdir = reflog.getParentFile(); + + if (!refdir.exists() && !refdir.mkdirs()) + throw new IOException("Cannot create directory " + refdir); + + final FileOutputStream out = new FileOutputStream(reflog, true); + try { + out.write(rec); + } finally { + out.close(); + } + } + + /** + * Writes reflog entry for ref specified by refName + * + * @param repo + * repository to use + * @param oldCommit + * previous commit + * @param commit + * new commit + * @param message + * reflog message + * @param refName + * full ref name + * @throws IOException + * @deprecated rely upon {@link RefUpdate}'s automatic logging instead. + */ + public static void writeReflog(Repository repo, ObjectId oldCommit, + ObjectId commit, String message, String refName) throws IOException { + appendOneRecord(oldCommit, commit, null, message, repo, refName); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefRename.java new file mode 100644 index 0000000000..7e76ac58a0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefRename.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2009, Robin Rosenberg + * Copyright (C) 2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; + +import org.eclipse.jgit.lib.RefUpdate.Result; + +/** + * A RefUpdate combination for renaming a ref + */ +public class RefRename { + private RefUpdate newToUpdate; + + private RefUpdate oldFromDelete; + + private Result renameResult = Result.NOT_ATTEMPTED; + + RefRename(final RefUpdate toUpdate, final RefUpdate fromUpdate) { + newToUpdate = toUpdate; + oldFromDelete = fromUpdate; + } + + /** + * @return result of rename operation + */ + public Result getResult() { + return renameResult; + } + + /** + * @return the result of the new ref update + * @throws IOException + */ + public Result rename() throws IOException { + Ref oldRef = oldFromDelete.db.readRef(Constants.HEAD); + boolean renameHEADtoo = oldRef != null + && oldRef.getName().equals(oldFromDelete.getName()); + Repository db = oldFromDelete.getRepository(); + try { + RefLogWriter.renameTo(db, oldFromDelete, + newToUpdate); + newToUpdate.setRefLogMessage(null, false); + String tmpRefName = "RENAMED-REF.." + Thread.currentThread().getId(); + RefUpdate tmpUpdateRef = db.updateRef(tmpRefName); + if (renameHEADtoo) { + try { + oldFromDelete.db.link(Constants.HEAD, tmpRefName); + } catch (IOException e) { + RefLogWriter.renameTo(db, + newToUpdate, oldFromDelete); + return renameResult = Result.LOCK_FAILURE; + } + } + tmpUpdateRef.setNewObjectId(oldFromDelete.getOldObjectId()); + tmpUpdateRef.setForceUpdate(true); + Result update = tmpUpdateRef.update(); + if (update != Result.FORCED && update != Result.NEW && update != Result.NO_CHANGE) { + RefLogWriter.renameTo(db, + newToUpdate, oldFromDelete); + if (renameHEADtoo) { + oldFromDelete.db.link(Constants.HEAD, oldFromDelete.getName()); + } + return renameResult = update; + } + + oldFromDelete.setExpectedOldObjectId(oldFromDelete.getOldObjectId()); + oldFromDelete.setForceUpdate(true); + Result delete = oldFromDelete.delete(); + if (delete != Result.FORCED) { + if (db.getRef( + oldFromDelete.getName()) != null) { + RefLogWriter.renameTo(db, + newToUpdate, oldFromDelete); + if (renameHEADtoo) { + oldFromDelete.db.link(Constants.HEAD, oldFromDelete + .getName()); + } + } + return renameResult = delete; + } + + newToUpdate.setNewObjectId(tmpUpdateRef.getNewObjectId()); + Result updateResult = newToUpdate.update(); + if (updateResult != Result.NEW) { + RefLogWriter.renameTo(db, newToUpdate, oldFromDelete); + if (renameHEADtoo) { + oldFromDelete.db.link(Constants.HEAD, oldFromDelete.getName()); + } + oldFromDelete.setExpectedOldObjectId(null); + oldFromDelete.setNewObjectId(oldFromDelete.getOldObjectId()); + oldFromDelete.setForceUpdate(true); + oldFromDelete.setRefLogMessage(null, false); + Result undelete = oldFromDelete.update(); + if (undelete != Result.NEW && undelete != Result.LOCK_FAILURE) + return renameResult = Result.IO_FAILURE; + return renameResult = Result.LOCK_FAILURE; + } + + if (renameHEADtoo) { + oldFromDelete.db.link(Constants.HEAD, newToUpdate.getName()); + } else { + db.fireRefsMaybeChanged(); + } + RefLogWriter.append(this, newToUpdate.getName(), "Branch: renamed " + + db.shortenRefName(oldFromDelete.getName()) + " to " + + db.shortenRefName(newToUpdate.getName())); + if (renameHEADtoo) + RefLogWriter.append(this, Constants.HEAD, "Branch: renamed " + + db.shortenRefName(oldFromDelete.getName()) + " to " + + db.shortenRefName(newToUpdate.getName())); + return renameResult = Result.RENAMED; + } catch (RuntimeException e) { + throw e; + } + } + + ObjectId getObjectId() { + return oldFromDelete.getOldObjectId(); + } + + Repository getRepository() { + return oldFromDelete.getRepository(); + } + + PersonIdent getRefLogIdent() { + return newToUpdate.getRefLogIdent(); + } + + String getToName() { + return newToUpdate.getName(); + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java new file mode 100644 index 0000000000..18dc582c8a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java @@ -0,0 +1,642 @@ +/* + * Copyright (C) 2008, Charles O'Farrell <charleso@charleso.org> + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.IOException; + +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.Ref.Storage; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Updates any locally stored ref. + */ +public class RefUpdate { + /** Status of an update request. */ + public static enum Result { + /** The ref update/delete has not been attempted by the caller. */ + NOT_ATTEMPTED, + + /** + * The ref could not be locked for update/delete. + * <p> + * This is generally a transient failure and is usually caused by + * another process trying to access the ref at the same time as this + * process was trying to update it. It is possible a future operation + * will be successful. + */ + LOCK_FAILURE, + + /** + * Same value already stored. + * <p> + * Both the old value and the new value are identical. No change was + * necessary for an update. For delete the branch is removed. + */ + NO_CHANGE, + + /** + * The ref was created locally for an update, but ignored for delete. + * <p> + * The ref did not exist when the update started, but it was created + * successfully with the new value. + */ + NEW, + + /** + * The ref had to be forcefully updated/deleted. + * <p> + * The ref already existed but its old value was not fully merged into + * the new value. The configuration permitted a forced update to take + * place, so ref now contains the new value. History associated with the + * objects not merged may no longer be reachable. + */ + FORCED, + + /** + * The ref was updated/deleted in a fast-forward way. + * <p> + * The tracking ref already existed and its old value was fully merged + * into the new value. No history was made unreachable. + */ + FAST_FORWARD, + + /** + * Not a fast-forward and not stored. + * <p> + * The tracking ref already existed but its old value was not fully + * merged into the new value. The configuration did not allow a forced + * update/delete to take place, so ref still contains the old value. No + * previous history was lost. + */ + REJECTED, + + /** + * Rejected because trying to delete the current branch. + * <p> + * Has no meaning for update. + */ + REJECTED_CURRENT_BRANCH, + + /** + * The ref was probably not updated/deleted because of I/O error. + * <p> + * Unexpected I/O error occurred when writing new ref. Such error may + * result in uncertain state, but most probably ref was not updated. + * <p> + * This kind of error doesn't include {@link #LOCK_FAILURE}, which is a + * different case. + */ + IO_FAILURE, + + /** + * The ref was renamed from another name + * <p> + */ + RENAMED + } + + /** Repository the ref is stored in. */ + final RefDatabase db; + + /** Location of the loose file holding the value of this ref. */ + final File looseFile; + + /** New value the caller wants this ref to have. */ + private ObjectId newValue; + + /** Does this specification ask for forced updated (rewind/reset)? */ + private boolean force; + + /** Identity to record action as within the reflog. */ + private PersonIdent refLogIdent; + + /** Message the caller wants included in the reflog. */ + private String refLogMessage; + + /** Should the Result value be appended to {@link #refLogMessage}. */ + private boolean refLogIncludeResult; + + /** Old value of the ref, obtained after we lock it. */ + private ObjectId oldValue; + + /** If non-null, the value {@link #oldValue} must have to continue. */ + private ObjectId expValue; + + /** Result of the update operation. */ + Result result = Result.NOT_ATTEMPTED; + + private final Ref ref; + + RefUpdate(final RefDatabase r, final Ref ref, final File f) { + db = r; + this.ref = ref; + oldValue = ref.getObjectId(); + looseFile = f; + refLogMessage = ""; + } + + /** @return the repository the updated ref resides in */ + public Repository getRepository() { + return db.getRepository(); + } + + /** + * Get the name of the ref this update will operate on. + * + * @return name of underlying ref. + */ + public String getName() { + return ref.getName(); + } + + /** + * Get the requested name of the ref thit update will operate on + * + * @return original (requested) name of the underlying ref. + */ + public String getOrigName() { + return ref.getOrigName(); + } + + /** + * Get the new value the ref will be (or was) updated to. + * + * @return new value. Null if the caller has not configured it. + */ + public ObjectId getNewObjectId() { + return newValue; + } + + /** + * Set the new value the ref will update to. + * + * @param id + * the new value. + */ + public void setNewObjectId(final AnyObjectId id) { + newValue = id.copy(); + } + + /** + * @return the expected value of the ref after the lock is taken, but before + * update occurs. Null to avoid the compare and swap test. Use + * {@link ObjectId#zeroId()} to indicate expectation of a + * non-existant ref. + */ + public ObjectId getExpectedOldObjectId() { + return expValue; + } + + /** + * @param id + * the expected value of the ref after the lock is taken, but + * before update occurs. Null to avoid the compare and swap test. + * Use {@link ObjectId#zeroId()} to indicate expectation of a + * non-existant ref. + */ + public void setExpectedOldObjectId(final AnyObjectId id) { + expValue = id != null ? id.toObjectId() : null; + } + + /** + * Check if this update wants to forcefully change the ref. + * + * @return true if this update should ignore merge tests. + */ + public boolean isForceUpdate() { + return force; + } + + /** + * Set if this update wants to forcefully change the ref. + * + * @param b + * true if this update should ignore merge tests. + */ + public void setForceUpdate(final boolean b) { + force = b; + } + + /** @return identity of the user making the change in the reflog. */ + public PersonIdent getRefLogIdent() { + return refLogIdent; + } + + /** + * Set the identity of the user appearing in the reflog. + * <p> + * The timestamp portion of the identity is ignored. A new identity with the + * current timestamp will be created automatically when the update occurs + * and the log record is written. + * + * @param pi + * identity of the user. If null the identity will be + * automatically determined based on the repository + * configuration. + */ + public void setRefLogIdent(final PersonIdent pi) { + refLogIdent = pi; + } + + /** + * Get the message to include in the reflog. + * + * @return message the caller wants to include in the reflog; null if the + * update should not be logged. + */ + public String getRefLogMessage() { + return refLogMessage; + } + + /** + * Set the message to include in the reflog. + * + * @param msg + * the message to describe this change. It may be null + * if appendStatus is null in order not to append to the reflog + * @param appendStatus + * true if the status of the ref change (fast-forward or + * forced-update) should be appended to the user supplied + * message. + */ + public void setRefLogMessage(final String msg, final boolean appendStatus) { + if (msg == null && !appendStatus) + disableRefLog(); + else if (msg == null && appendStatus) { + refLogMessage = ""; + refLogIncludeResult = true; + } else { + refLogMessage = msg; + refLogIncludeResult = appendStatus; + } + } + + /** Don't record this update in the ref's associated reflog. */ + public void disableRefLog() { + refLogMessage = null; + refLogIncludeResult = false; + } + + /** + * The old value of the ref, prior to the update being attempted. + * <p> + * This value may differ before and after the update method. Initially it is + * populated with the value of the ref before the lock is taken, but the old + * value may change if someone else modified the ref between the time we + * last read it and when the ref was locked for update. + * + * @return the value of the ref prior to the update being attempted; null if + * the updated has not been attempted yet. + */ + public ObjectId getOldObjectId() { + return oldValue; + } + + /** + * Get the status of this update. + * <p> + * The same value that was previously returned from an update method. + * + * @return the status of the update. + */ + public Result getResult() { + return result; + } + + private void requireCanDoUpdate() { + if (newValue == null) + throw new IllegalStateException("A NewObjectId is required."); + } + + /** + * Force the ref to take the new value. + * <p> + * This is just a convenient helper for setting the force flag, and as such + * the merge test is performed. + * + * @return the result status of the update. + * @throws IOException + * an unexpected IO error occurred while writing changes. + */ + public Result forceUpdate() throws IOException { + force = true; + return update(); + } + + /** + * Gracefully update the ref to the new value. + * <p> + * Merge test will be performed according to {@link #isForceUpdate()}. + * <p> + * This is the same as: + * + * <pre> + * return update(new RevWalk(repository)); + * </pre> + * + * @return the result status of the update. + * @throws IOException + * an unexpected IO error occurred while writing changes. + */ + public Result update() throws IOException { + return update(new RevWalk(db.getRepository())); + } + + /** + * Gracefully update the ref to the new value. + * <p> + * Merge test will be performed according to {@link #isForceUpdate()}. + * + * @param walk + * a RevWalk instance this update command can borrow to perform + * the merge test. The walk will be reset to perform the test. + * @return the result status of the update. + * @throws IOException + * an unexpected IO error occurred while writing changes. + */ + public Result update(final RevWalk walk) throws IOException { + requireCanDoUpdate(); + try { + return result = updateImpl(walk, new UpdateStore()); + } catch (IOException x) { + result = Result.IO_FAILURE; + throw x; + } + } + + /** + * Delete the ref. + * <p> + * This is the same as: + * + * <pre> + * return delete(new RevWalk(repository)); + * </pre> + * + * @return the result status of the delete. + * @throws IOException + */ + public Result delete() throws IOException { + return delete(new RevWalk(db.getRepository())); + } + + /** + * Delete the ref. + * + * @param walk + * a RevWalk instance this delete command can borrow to perform + * the merge test. The walk will be reset to perform the test. + * @return the result status of the delete. + * @throws IOException + */ + public Result delete(final RevWalk walk) throws IOException { + if (getName().startsWith(Constants.R_HEADS)) { + final Ref head = db.readRef(Constants.HEAD); + if (head != null && getName().equals(head.getName())) + return result = Result.REJECTED_CURRENT_BRANCH; + } + + try { + return result = updateImpl(walk, new DeleteStore()); + } catch (IOException x) { + result = Result.IO_FAILURE; + throw x; + } + } + + private Result updateImpl(final RevWalk walk, final Store store) + throws IOException { + final LockFile lock; + RevObject newObj; + RevObject oldObj; + + if (isNameConflicting()) + return Result.LOCK_FAILURE; + lock = new LockFile(looseFile); + if (!lock.lock()) + return Result.LOCK_FAILURE; + try { + oldValue = db.idOf(getName()); + if (expValue != null) { + final ObjectId o; + o = oldValue != null ? oldValue : ObjectId.zeroId(); + if (!AnyObjectId.equals(expValue, o)) + return Result.LOCK_FAILURE; + } + if (oldValue == null) + return store.store(lock, Result.NEW); + + newObj = safeParse(walk, newValue); + oldObj = safeParse(walk, oldValue); + if (newObj == oldObj) + return store.store(lock, Result.NO_CHANGE); + + if (newObj instanceof RevCommit && oldObj instanceof RevCommit) { + if (walk.isMergedInto((RevCommit) oldObj, (RevCommit) newObj)) + return store.store(lock, Result.FAST_FORWARD); + } + + if (isForceUpdate()) + return store.store(lock, Result.FORCED); + return Result.REJECTED; + } finally { + lock.unlock(); + } + } + + private boolean isNameConflicting() throws IOException { + final String myName = getName(); + final int lastSlash = myName.lastIndexOf('/'); + if (lastSlash > 0) + if (db.getRepository().getRef(myName.substring(0, lastSlash)) != null) + return true; + + final String rName = myName + "/"; + for (Ref r : db.getAllRefs().values()) { + if (r.getName().startsWith(rName)) + return true; + } + return false; + } + + private static RevObject safeParse(final RevWalk rw, final AnyObjectId id) + throws IOException { + try { + return id != null ? rw.parseAny(id) : null; + } catch (MissingObjectException e) { + // We can expect some objects to be missing, like if we are + // trying to force a deletion of a branch and the object it + // points to has been pruned from the database due to freak + // corruption accidents (it happens with 'git new-work-dir'). + // + return null; + } + } + + private Result updateStore(final LockFile lock, final Result status) + throws IOException { + if (status == Result.NO_CHANGE) + return status; + lock.setNeedStatInformation(true); + lock.write(newValue); + String msg = getRefLogMessage(); + if (msg != null) { + if (refLogIncludeResult) { + String strResult = toResultString(status); + if (strResult != null) { + if (msg.length() > 0) + msg = msg + ": " + strResult; + else + msg = strResult; + } + } + RefLogWriter.append(this, msg); + } + if (!lock.commit()) + return Result.LOCK_FAILURE; + db.stored(this.ref.getOrigName(), ref.getName(), newValue, lock.getCommitLastModified()); + return status; + } + + private static String toResultString(final Result status) { + switch (status) { + case FORCED: + return "forced-update"; + case FAST_FORWARD: + return "fast forward"; + case NEW: + return "created"; + default: + return null; + } + } + + /** + * Handle the abstraction of storing a ref update. This is because both + * updating and deleting of a ref have merge testing in common. + */ + private abstract class Store { + abstract Result store(final LockFile lock, final Result status) + throws IOException; + } + + class UpdateStore extends Store { + + @Override + Result store(final LockFile lock, final Result status) + throws IOException { + return updateStore(lock, status); + } + } + + class DeleteStore extends Store { + + @Override + Result store(LockFile lock, Result status) throws IOException { + Storage storage = ref.getStorage(); + if (storage == Storage.NEW) + return status; + if (storage.isPacked()) + db.removePackedRef(ref.getName()); + + final int levels = count(ref.getName(), '/') - 2; + + // Delete logs _before_ unlocking + final File gitDir = db.getRepository().getDirectory(); + final File logDir = new File(gitDir, Constants.LOGS); + deleteFileAndEmptyDir(new File(logDir, ref.getName()), levels); + + // We have to unlock before (maybe) deleting the parent directories + lock.unlock(); + if (storage.isLoose()) + deleteFileAndEmptyDir(looseFile, levels); + db.uncacheRef(ref.getName()); + return status; + } + + private void deleteFileAndEmptyDir(final File file, final int depth) + throws IOException { + if (file.isFile()) { + if (!file.delete()) + throw new IOException("File cannot be deleted: " + file); + File dir = file.getParentFile(); + for (int i = 0; i < depth; ++i) { + if (!dir.delete()) + break; // ignore problem here + dir = dir.getParentFile(); + } + } + } + } + + UpdateStore newUpdateStore() { + return new UpdateStore(); + } + + DeleteStore newDeleteStore() { + return new DeleteStore(); + } + + static void deleteEmptyDir(File dir, int depth) { + for (; depth > 0 && dir != null; depth--) { + if (dir.exists() && !dir.delete()) + break; + dir = dir.getParentFile(); + } + } + + static int count(final String s, final char c) { + int count = 0; + for (int p = s.indexOf(c); p >= 0; p = s.indexOf(c, p + 1)) { + count++; + } + return count; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java new file mode 100644 index 0000000000..34e73a3f7d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2008, Charles O'Farrell <charleso@charleso.org> + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Collection; + +/** + * Writes out refs to the {@link Constants#INFO_REFS} and + * {@link Constants#PACKED_REFS} files. + * + * This class is abstract as the writing of the files must be handled by the + * caller. This is because it is used by transport classes as well. + */ +public abstract class RefWriter { + + private final Collection<Ref> refs; + + /** + * @param refs + * the complete set of references. This should have been computed + * by applying updates to the advertised refs already discovered. + */ + public RefWriter(Collection<Ref> refs) { + this.refs = RefComparator.sort(refs); + } + + /** + * Rebuild the {@link Constants#INFO_REFS}. + * <p> + * This method rebuilds the contents of the {@link Constants#INFO_REFS} file + * to match the passed list of references. + * + * + * @throws IOException + * writing is not supported, or attempting to write the file + * failed, possibly due to permissions or remote disk full, etc. + */ + public void writeInfoRefs() throws IOException { + final StringWriter w = new StringWriter(); + final char[] tmp = new char[Constants.OBJECT_ID_LENGTH * 2]; + for (final Ref r : refs) { + if (Constants.HEAD.equals(r.getName())) { + // Historically HEAD has never been published through + // the INFO_REFS file. This is a mistake, but its the + // way things are. + // + continue; + } + + r.getObjectId().copyTo(tmp, w); + w.write('\t'); + w.write(r.getName()); + w.write('\n'); + + if (r.getPeeledObjectId() != null) { + r.getPeeledObjectId().copyTo(tmp, w); + w.write('\t'); + w.write(r.getName()); + w.write("^{}\n"); + } + } + writeFile(Constants.INFO_REFS, Constants.encode(w.toString())); + } + + /** + * Rebuild the {@link Constants#PACKED_REFS} file. + * <p> + * This method rebuilds the contents of the {@link Constants#PACKED_REFS} + * file to match the passed list of references, including only those refs + * that have a storage type of {@link Ref.Storage#PACKED}. + * + * @throws IOException + * writing is not supported, or attempting to write the file + * failed, possibly due to permissions or remote disk full, etc. + */ + public void writePackedRefs() throws IOException { + boolean peeled = false; + + for (final Ref r : refs) { + if (r.getStorage() != Ref.Storage.PACKED) + continue; + if (r.getPeeledObjectId() != null) + peeled = true; + } + + final StringWriter w = new StringWriter(); + if (peeled) { + w.write("# pack-refs with:"); + if (peeled) + w.write(" peeled"); + w.write('\n'); + } + + final char[] tmp = new char[Constants.OBJECT_ID_LENGTH * 2]; + for (final Ref r : refs) { + if (r.getStorage() != Ref.Storage.PACKED) + continue; + + r.getObjectId().copyTo(tmp, w); + w.write(' '); + w.write(r.getName()); + w.write('\n'); + + if (r.getPeeledObjectId() != null) { + w.write('^'); + r.getPeeledObjectId().copyTo(tmp, w); + w.write('\n'); + } + } + writeFile(Constants.PACKED_REFS, Constants.encode(w.toString())); + } + + /** + * Handles actual writing of ref files to the git repository, which may + * differ slightly depending on the destination and transport. + * + * @param file + * path to ref file. + * @param content + * byte content of file to be written. + * @throws IOException + */ + protected abstract void writeFile(String file, byte[] content) + throws IOException; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogReader.java new file mode 100644 index 0000000000..a85eb0e4c6 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogReader.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2009, Robin Rosenberg + * Copyright (C) 2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * Utility for reading reflog entries + */ +public class ReflogReader { + /** + * Parsed reflog entry + */ + static public class Entry { + private ObjectId oldId; + + private ObjectId newId; + + private PersonIdent who; + + private String comment; + + Entry(byte[] raw, int pos) { + oldId = ObjectId.fromString(raw, pos); + pos += Constants.OBJECT_ID_LENGTH * 2; + if (raw[pos++] != ' ') + throw new IllegalArgumentException( + "Raw log message does not parse as log entry"); + newId = ObjectId.fromString(raw, pos); + pos += Constants.OBJECT_ID_LENGTH * 2; + if (raw[pos++] != ' ') { + throw new IllegalArgumentException( + "Raw log message does not parse as log entry"); + } + who = RawParseUtils.parsePersonIdentOnly(raw, pos); + int p0 = RawParseUtils.next(raw, pos, '\t'); // personident has no + // \t + if (p0 == -1) { + throw new IllegalArgumentException( + "Raw log message does not parse as log entry"); + } + int p1 = RawParseUtils.nextLF(raw, p0); + if (p1 == -1) { + throw new IllegalArgumentException( + "Raw log message does not parse as log entry"); + } + comment = RawParseUtils.decode(raw, p0, p1 - 1); + } + + /** + * @return the commit id before the change + */ + public ObjectId getOldId() { + return oldId; + } + + /** + * @return the commit id after the change + */ + public ObjectId getNewId() { + return newId; + } + + /** + * @return user performin the change + */ + public PersonIdent getWho() { + return who; + } + + /** + * @return textual description of the change + */ + public String getComment() { + return comment; + } + + @Override + public String toString() { + return "Entry[" + oldId.name() + ", " + newId.name() + ", " + getWho() + ", " + + getComment() + "]"; + } + } + + private File logName; + + ReflogReader(Repository db, String refname) { + logName = new File(db.getDirectory(), "logs/" + refname); + } + + /** + * Get the last entry in the reflog + * + * @return the latest reflog entry, or null if no log + * @throws IOException + */ + public Entry getLastEntry() throws IOException { + List<Entry> entries = getReverseEntries(1); + return entries.size() > 0 ? entries.get(0) : null; + } + + /** + * @return all reflog entries in reverse order + * @throws IOException + */ + public List<Entry> getReverseEntries() throws IOException { + return getReverseEntries(Integer.MAX_VALUE); + } + + /** + * @param max + * max numer of entries to read + * @return all reflog entries in reverse order + * @throws IOException + */ + public List<Entry> getReverseEntries(int max) throws IOException { + final byte[] log; + try { + log = NB.readFully(logName); + } catch (FileNotFoundException e) { + return Collections.emptyList(); + } + + int rs = RawParseUtils.prevLF(log, log.length); + List<Entry> ret = new ArrayList<Entry>(); + while (rs >= 0 && max-- > 0) { + rs = RawParseUtils.prevLF(log, rs); + Entry entry = new Entry(log, rs < 0 ? 0 : rs + 2); + ret.add(entry); + } + return ret; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefsChangedEvent.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefsChangedEvent.java new file mode 100644 index 0000000000..7a7d99cd23 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefsChangedEvent.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +/** + * This class passes information about a changed Git index to a + * {@link RepositoryListener} + * + * Currently only a reference to the repository is passed. + */ +public class RefsChangedEvent extends RepositoryChangedEvent { + RefsChangedEvent(final Repository repository) { + super(repository); + } + + @Override + public String toString() { + return "RefsChangedEvent[" + getRepository() + "]"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java new file mode 100644 index 0000000000..181ca580f6 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -0,0 +1,1176 @@ +/* + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2006-2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Vector; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.SystemReader; + +/** + * Represents a Git repository. A repository holds all objects and refs used for + * managing source code (could by any type of file, but source code is what + * SCM's are typically used for). + * + * In Git terms all data is stored in GIT_DIR, typically a directory called + * .git. A work tree is maintained unless the repository is a bare repository. + * Typically the .git directory is located at the root of the work dir. + * + * <ul> + * <li>GIT_DIR + * <ul> + * <li>objects/ - objects</li> + * <li>refs/ - tags and heads</li> + * <li>config - configuration</li> + * <li>info/ - more configurations</li> + * </ul> + * </li> + * </ul> + * <p> + * This class is thread-safe. + * <p> + * This implementation only handles a subtly undocumented subset of git features. + * + */ +public class Repository { + private final AtomicInteger useCnt = new AtomicInteger(1); + + private final File gitDir; + + private final RepositoryConfig config; + + private final RefDatabase refs; + + private final ObjectDirectory objectDatabase; + + private GitIndex index; + + private final List<RepositoryListener> listeners = new Vector<RepositoryListener>(); // thread safe + static private final List<RepositoryListener> allListeners = new Vector<RepositoryListener>(); // thread safe + + /** + * Construct a representation of a Git repository. + * + * @param d + * GIT_DIR (the location of the repository metadata). + * @throws IOException + * the repository appears to already exist but cannot be + * accessed. + */ + public Repository(final File d) throws IOException { + gitDir = d.getAbsoluteFile(); + refs = new RefDatabase(this); + objectDatabase = new ObjectDirectory(FS.resolve(gitDir, "objects")); + + final FileBasedConfig userConfig; + userConfig = SystemReader.getInstance().openUserConfig(); + try { + userConfig.load(); + } catch (ConfigInvalidException e1) { + IOException e2 = new IOException("User config file " + + userConfig.getFile().getAbsolutePath() + " invalid: " + + e1); + e2.initCause(e1); + throw e2; + } + config = new RepositoryConfig(userConfig, FS.resolve(gitDir, "config")); + + if (objectDatabase.exists()) { + try { + getConfig().load(); + } catch (ConfigInvalidException e1) { + IOException e2 = new IOException("Unknown repository format"); + e2.initCause(e1); + throw e2; + } + final String repositoryFormatVersion = getConfig().getString( + "core", null, "repositoryFormatVersion"); + if (!"0".equals(repositoryFormatVersion)) { + throw new IOException("Unknown repository format \"" + + repositoryFormatVersion + "\"; expected \"0\"."); + } + } + } + + /** + * Create a new Git repository initializing the necessary files and + * directories. Repository with working tree is created using this method. + * + * @throws IOException + * @see #create(boolean) + */ + public synchronized void create() throws IOException { + create(false); + } + + /** + * Create a new Git repository initializing the necessary files and + * directories. + * + * @param bare + * if true, a bare repository is created. + * + * @throws IOException + * in case of IO problem + */ + public void create(boolean bare) throws IOException { + final RepositoryConfig cfg = getConfig(); + if (cfg.getFile().exists()) { + throw new IllegalStateException("Repository already exists: " + + gitDir); + } + gitDir.mkdirs(); + refs.create(); + objectDatabase.create(); + + new File(gitDir, "branches").mkdir(); + new File(gitDir, "remotes").mkdir(); + final String master = Constants.R_HEADS + Constants.MASTER; + refs.link(Constants.HEAD, master); + + cfg.setInt("core", null, "repositoryformatversion", 0); + cfg.setBoolean("core", null, "filemode", true); + if (bare) + cfg.setBoolean("core", null, "bare", true); + cfg.save(); + } + + /** + * @return GIT_DIR + */ + public File getDirectory() { + return gitDir; + } + + /** + * @return the directory containing the objects owned by this repository. + */ + public File getObjectsDirectory() { + return objectDatabase.getDirectory(); + } + + /** + * @return the object database which stores this repository's data. + */ + public ObjectDatabase getObjectDatabase() { + return objectDatabase; + } + + /** + * @return the configuration of this repository + */ + public RepositoryConfig getConfig() { + return config; + } + + /** + * Construct a filename where the loose object having a specified SHA-1 + * should be stored. If the object is stored in a shared repository the path + * to the alternative repo will be returned. If the object is not yet store + * a usable path in this repo will be returned. It is assumed that callers + * will look for objects in a pack first. + * + * @param objectId + * @return suggested file name + */ + public File toFile(final AnyObjectId objectId) { + return objectDatabase.fileFor(objectId); + } + + /** + * @param objectId + * @return true if the specified object is stored in this repo or any of the + * known shared repositories. + */ + public boolean hasObject(final AnyObjectId objectId) { + return objectDatabase.hasObject(objectId); + } + + /** + * @param id + * SHA-1 of an object. + * + * @return a {@link ObjectLoader} for accessing the data of the named + * object, or null if the object does not exist. + * @throws IOException + */ + public ObjectLoader openObject(final AnyObjectId id) + throws IOException { + final WindowCursor wc = new WindowCursor(); + try { + return openObject(wc, id); + } finally { + wc.release(); + } + } + + /** + * @param curs + * temporary working space associated with the calling thread. + * @param id + * SHA-1 of an object. + * + * @return a {@link ObjectLoader} for accessing the data of the named + * object, or null if the object does not exist. + * @throws IOException + */ + public ObjectLoader openObject(final WindowCursor curs, final AnyObjectId id) + throws IOException { + return objectDatabase.openObject(curs, id); + } + + /** + * Open object in all packs containing specified object. + * + * @param objectId + * id of object to search for + * @param curs + * temporary working space associated with the calling thread. + * @return collection of loaders for this object, from all packs containing + * this object + * @throws IOException + */ + public Collection<PackedObjectLoader> openObjectInAllPacks( + final AnyObjectId objectId, final WindowCursor curs) + throws IOException { + Collection<PackedObjectLoader> result = new LinkedList<PackedObjectLoader>(); + openObjectInAllPacks(objectId, result, curs); + return result; + } + + /** + * Open object in all packs containing specified object. + * + * @param objectId + * id of object to search for + * @param resultLoaders + * result collection of loaders for this object, filled with + * loaders from all packs containing specified object + * @param curs + * temporary working space associated with the calling thread. + * @throws IOException + */ + void openObjectInAllPacks(final AnyObjectId objectId, + final Collection<PackedObjectLoader> resultLoaders, + final WindowCursor curs) throws IOException { + objectDatabase.openObjectInAllPacks(resultLoaders, curs, objectId); + } + + /** + * @param id + * SHA'1 of a blob + * @return an {@link ObjectLoader} for accessing the data of a named blob + * @throws IOException + */ + public ObjectLoader openBlob(final ObjectId id) throws IOException { + return openObject(id); + } + + /** + * @param id + * SHA'1 of a tree + * @return an {@link ObjectLoader} for accessing the data of a named tree + * @throws IOException + */ + public ObjectLoader openTree(final ObjectId id) throws IOException { + return openObject(id); + } + + /** + * Access a Commit object using a symbolic reference. This reference may + * be a SHA-1 or ref in combination with a number of symbols translating + * from one ref or SHA1-1 to another, such as HEAD^ etc. + * + * @param revstr a reference to a git commit object + * @return a Commit named by the specified string + * @throws IOException for I/O error or unexpected object type. + * + * @see #resolve(String) + */ + public Commit mapCommit(final String revstr) throws IOException { + final ObjectId id = resolve(revstr); + return id != null ? mapCommit(id) : null; + } + + /** + * Access any type of Git object by id and + * + * @param id + * SHA-1 of object to read + * @param refName optional, only relevant for simple tags + * @return The Git object if found or null + * @throws IOException + */ + public Object mapObject(final ObjectId id, final String refName) throws IOException { + final ObjectLoader or = openObject(id); + if (or == null) + return null; + final byte[] raw = or.getBytes(); + switch (or.getType()) { + case Constants.OBJ_TREE: + return makeTree(id, raw); + + case Constants.OBJ_COMMIT: + return makeCommit(id, raw); + + case Constants.OBJ_TAG: + return makeTag(id, refName, raw); + + case Constants.OBJ_BLOB: + return raw; + + default: + throw new IncorrectObjectTypeException(id, + "COMMIT nor TREE nor BLOB nor TAG"); + } + } + + /** + * Access a Commit by SHA'1 id. + * @param id + * @return Commit or null + * @throws IOException for I/O error or unexpected object type. + */ + public Commit mapCommit(final ObjectId id) throws IOException { + final ObjectLoader or = openObject(id); + if (or == null) + return null; + final byte[] raw = or.getBytes(); + if (Constants.OBJ_COMMIT == or.getType()) + return new Commit(this, id, raw); + throw new IncorrectObjectTypeException(id, Constants.TYPE_COMMIT); + } + + private Commit makeCommit(final ObjectId id, final byte[] raw) { + Commit ret = new Commit(this, id, raw); + return ret; + } + + /** + * Access a Tree object using a symbolic reference. This reference may + * be a SHA-1 or ref in combination with a number of symbols translating + * from one ref or SHA1-1 to another, such as HEAD^{tree} etc. + * + * @param revstr a reference to a git commit object + * @return a Tree named by the specified string + * @throws IOException + * + * @see #resolve(String) + */ + public Tree mapTree(final String revstr) throws IOException { + final ObjectId id = resolve(revstr); + return id != null ? mapTree(id) : null; + } + + /** + * Access a Tree by SHA'1 id. + * @param id + * @return Tree or null + * @throws IOException for I/O error or unexpected object type. + */ + public Tree mapTree(final ObjectId id) throws IOException { + final ObjectLoader or = openObject(id); + if (or == null) + return null; + final byte[] raw = or.getBytes(); + switch (or.getType()) { + case Constants.OBJ_TREE: + return new Tree(this, id, raw); + + case Constants.OBJ_COMMIT: + return mapTree(ObjectId.fromString(raw, 5)); + + default: + throw new IncorrectObjectTypeException(id, Constants.TYPE_TREE); + } + } + + private Tree makeTree(final ObjectId id, final byte[] raw) throws IOException { + Tree ret = new Tree(this, id, raw); + return ret; + } + + private Tag makeTag(final ObjectId id, final String refName, final byte[] raw) { + Tag ret = new Tag(this, id, refName, raw); + return ret; + } + + /** + * Access a tag by symbolic name. + * + * @param revstr + * @return a Tag or null + * @throws IOException on I/O error or unexpected type + */ + public Tag mapTag(String revstr) throws IOException { + final ObjectId id = resolve(revstr); + return id != null ? mapTag(revstr, id) : null; + } + + /** + * Access a Tag by SHA'1 id + * @param refName + * @param id + * @return Commit or null + * @throws IOException for I/O error or unexpected object type. + */ + public Tag mapTag(final String refName, final ObjectId id) throws IOException { + final ObjectLoader or = openObject(id); + if (or == null) + return null; + final byte[] raw = or.getBytes(); + if (Constants.OBJ_TAG == or.getType()) + return new Tag(this, id, refName, raw); + return new Tag(this, id, refName, null); + } + + /** + * Create a command to update, create or delete a ref in this repository. + * + * @param ref + * name of the ref the caller wants to modify. + * @return an update command. The caller must finish populating this command + * and then invoke one of the update methods to actually make a + * change. + * @throws IOException + * a symbolic ref was passed in and could not be resolved back + * to the base ref, as the symbolic ref could not be read. + */ + public RefUpdate updateRef(final String ref) throws IOException { + return refs.newUpdate(ref); + } + + /** + * Create a command to rename a ref in this repository + * + * @param fromRef + * name of ref to rename from + * @param toRef + * name of ref to rename to + * @return an update command that knows how to rename a branch to another. + * @throws IOException + * the rename could not be performed. + * + */ + public RefRename renameRef(final String fromRef, final String toRef) throws IOException { + return refs.newRename(fromRef, toRef); + } + + /** + * Parse a git revision string and return an object id. + * + * Currently supported is combinations of these. + * <ul> + * <li>SHA-1 - a SHA-1</li> + * <li>refs/... - a ref name</li> + * <li>ref^n - nth parent reference</li> + * <li>ref~n - distance via parent reference</li> + * <li>ref@{n} - nth version of ref</li> + * <li>ref^{tree} - tree references by ref</li> + * <li>ref^{commit} - commit references by ref</li> + * </ul> + * + * Not supported is + * <ul> + * <li>timestamps in reflogs, ref@{full or relative timestamp}</li> + * <li>abbreviated SHA-1's</li> + * </ul> + * + * @param revstr A git object references expression + * @return an ObjectId or null if revstr can't be resolved to any ObjectId + * @throws IOException on serious errors + */ + public ObjectId resolve(final String revstr) throws IOException { + char[] rev = revstr.toCharArray(); + Object ref = null; + ObjectId refId = null; + for (int i = 0; i < rev.length; ++i) { + switch (rev[i]) { + case '^': + if (refId == null) { + String refstr = new String(rev,0,i); + refId = resolveSimple(refstr); + if (refId == null) + return null; + } + if (i + 1 < rev.length) { + switch (rev[i + 1]) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + int j; + ref = mapObject(refId, null); + while (ref instanceof Tag) { + Tag tag = (Tag)ref; + refId = tag.getObjId(); + ref = mapObject(refId, null); + } + if (!(ref instanceof Commit)) + throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT); + for (j=i+1; j<rev.length; ++j) { + if (!Character.isDigit(rev[j])) + break; + } + String parentnum = new String(rev, i+1, j-i-1); + int pnum; + try { + pnum = Integer.parseInt(parentnum); + } catch (NumberFormatException e) { + throw new RevisionSyntaxException( + "Invalid commit parent number", + revstr); + } + if (pnum != 0) { + final ObjectId parents[] = ((Commit) ref) + .getParentIds(); + if (pnum > parents.length) + refId = null; + else + refId = parents[pnum - 1]; + } + i = j - 1; + break; + case '{': + int k; + String item = null; + for (k=i+2; k<rev.length; ++k) { + if (rev[k] == '}') { + item = new String(rev, i+2, k-i-2); + break; + } + } + i = k; + if (item != null) + if (item.equals("tree")) { + ref = mapObject(refId, null); + while (ref instanceof Tag) { + Tag t = (Tag)ref; + refId = t.getObjId(); + ref = mapObject(refId, null); + } + if (ref instanceof Treeish) + refId = ((Treeish)ref).getTreeId(); + else + throw new IncorrectObjectTypeException(refId, Constants.TYPE_TREE); + } + else if (item.equals("commit")) { + ref = mapObject(refId, null); + while (ref instanceof Tag) { + Tag t = (Tag)ref; + refId = t.getObjId(); + ref = mapObject(refId, null); + } + if (!(ref instanceof Commit)) + throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT); + } + else if (item.equals("blob")) { + ref = mapObject(refId, null); + while (ref instanceof Tag) { + Tag t = (Tag)ref; + refId = t.getObjId(); + ref = mapObject(refId, null); + } + if (!(ref instanceof byte[])) + throw new IncorrectObjectTypeException(refId, Constants.TYPE_BLOB); + } + else if (item.equals("")) { + ref = mapObject(refId, null); + while (ref instanceof Tag) { + Tag t = (Tag)ref; + refId = t.getObjId(); + ref = mapObject(refId, null); + } + } + else + throw new RevisionSyntaxException(revstr); + else + throw new RevisionSyntaxException(revstr); + break; + default: + ref = mapObject(refId, null); + if (ref instanceof Commit) { + final ObjectId parents[] = ((Commit) ref) + .getParentIds(); + if (parents.length == 0) + refId = null; + else + refId = parents[0]; + } else + throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT); + + } + } else { + ref = mapObject(refId, null); + while (ref instanceof Tag) { + Tag tag = (Tag)ref; + refId = tag.getObjId(); + ref = mapObject(refId, null); + } + if (ref instanceof Commit) { + final ObjectId parents[] = ((Commit) ref) + .getParentIds(); + if (parents.length == 0) + refId = null; + else + refId = parents[0]; + } else + throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT); + } + break; + case '~': + if (ref == null) { + String refstr = new String(rev,0,i); + refId = resolveSimple(refstr); + if (refId == null) + return null; + ref = mapObject(refId, null); + } + while (ref instanceof Tag) { + Tag tag = (Tag)ref; + refId = tag.getObjId(); + ref = mapObject(refId, null); + } + if (!(ref instanceof Commit)) + throw new IncorrectObjectTypeException(refId, Constants.TYPE_COMMIT); + int l; + for (l = i + 1; l < rev.length; ++l) { + if (!Character.isDigit(rev[l])) + break; + } + String distnum = new String(rev, i+1, l-i-1); + int dist; + try { + dist = Integer.parseInt(distnum); + } catch (NumberFormatException e) { + throw new RevisionSyntaxException( + "Invalid ancestry length", revstr); + } + while (dist > 0) { + final ObjectId[] parents = ((Commit) ref).getParentIds(); + if (parents.length == 0) { + refId = null; + break; + } + refId = parents[0]; + ref = mapCommit(refId); + --dist; + } + i = l - 1; + break; + case '@': + int m; + String time = null; + for (m=i+2; m<rev.length; ++m) { + if (rev[m] == '}') { + time = new String(rev, i+2, m-i-2); + break; + } + } + if (time != null) + throw new RevisionSyntaxException("reflogs not yet supported by revision parser", revstr); + i = m - 1; + break; + default: + if (refId != null) + throw new RevisionSyntaxException(revstr); + } + } + if (refId == null) + refId = resolveSimple(revstr); + return refId; + } + + private ObjectId resolveSimple(final String revstr) throws IOException { + if (ObjectId.isId(revstr)) + return ObjectId.fromString(revstr); + final Ref r = refs.readRef(revstr); + return r != null ? r.getObjectId() : null; + } + + /** Increment the use counter by one, requiring a matched {@link #close()}. */ + public void incrementOpen() { + useCnt.incrementAndGet(); + } + + /** + * Close all resources used by this repository + */ + public void close() { + if (useCnt.decrementAndGet() == 0) + objectDatabase.close(); + } + + /** + * Add a single existing pack to the list of available pack files. + * + * @param pack + * path of the pack file to open. + * @param idx + * path of the corresponding index file. + * @throws IOException + * index file could not be opened, read, or is not recognized as + * a Git pack file index. + */ + public void openPack(final File pack, final File idx) throws IOException { + objectDatabase.openPack(pack, idx); + } + + /** + * Writes a symref (e.g. HEAD) to disk + * + * @param name symref name + * @param target pointed to ref + * @throws IOException + */ + public void writeSymref(final String name, final String target) + throws IOException { + refs.link(name, target); + } + + public String toString() { + return "Repository[" + getDirectory() + "]"; + } + + /** + * @return name of current branch + * @throws IOException + */ + public String getFullBranch() throws IOException { + final File ptr = new File(getDirectory(),Constants.HEAD); + final BufferedReader br = new BufferedReader(new FileReader(ptr)); + String ref; + try { + ref = br.readLine(); + } finally { + br.close(); + } + if (ref.startsWith("ref: ")) + ref = ref.substring(5); + return ref; + } + + /** + * @return name of current branch. + * @throws IOException + */ + public String getBranch() throws IOException { + try { + final File ptr = new File(getDirectory(), Constants.HEAD); + final BufferedReader br = new BufferedReader(new FileReader(ptr)); + String ref; + try { + ref = br.readLine(); + } finally { + br.close(); + } + if (ref.startsWith("ref: ")) + ref = ref.substring(5); + if (ref.startsWith("refs/heads/")) + ref = ref.substring(11); + return ref; + } catch (FileNotFoundException e) { + final File ptr = new File(getDirectory(),"head-name"); + final BufferedReader br = new BufferedReader(new FileReader(ptr)); + String ref; + try { + ref = br.readLine(); + } finally { + br.close(); + } + return ref; + } + } + + /** + * Get a ref by name. + * + * @param name + * the name of the ref to lookup. May be a short-hand form, e.g. + * "master" which is is automatically expanded to + * "refs/heads/master" if "refs/heads/master" already exists. + * @return the Ref with the given name, or null if it does not exist + * @throws IOException + */ + public Ref getRef(final String name) throws IOException { + return refs.readRef(name); + } + + /** + * @return all known refs (heads, tags, remotes). + */ + public Map<String, Ref> getAllRefs() { + return refs.getAllRefs(); + } + + /** + * @return all tags; key is short tag name ("v1.0") and value of the entry + * contains the ref with the full tag name ("refs/tags/v1.0"). + */ + public Map<String, Ref> getTags() { + return refs.getTags(); + } + + /** + * Peel a possibly unpeeled ref and updates it. + * <p> + * If the ref cannot be peeled (as it does not refer to an annotated tag) + * the peeled id stays null, but {@link Ref#isPeeled()} will be true. + * + * @param ref + * The ref to peel + * @return <code>ref</code> if <code>ref.isPeeled()</code> is true; else a + * new Ref object representing the same data as Ref, but isPeeled() + * will be true and getPeeledObjectId will contain the peeled object + * (or null). + */ + public Ref peel(final Ref ref) { + return refs.peel(ref); + } + + /** + * @return a map with all objects referenced by a peeled ref. + */ + public Map<AnyObjectId, Set<Ref>> getAllRefsByPeeledObjectId() { + Map<String, Ref> allRefs = getAllRefs(); + Map<AnyObjectId, Set<Ref>> ret = new HashMap<AnyObjectId, Set<Ref>>(allRefs.size()); + for (Ref ref : allRefs.values()) { + if (!ref.isPeeled()) + ref = peel(ref); + AnyObjectId target = ref.getPeeledObjectId(); + if (target == null) + target = ref.getObjectId(); + // We assume most Sets here are singletons + Set<Ref> oset = ret.put(target, Collections.singleton(ref)); + if (oset != null) { + // that was not the case (rare) + if (oset.size() == 1) { + // Was a read-only singleton, we must copy to a new Set + oset = new HashSet<Ref>(oset); + } + ret.put(target, oset); + oset.add(ref); + } + } + return ret; + } + + /** Clean up stale caches */ + public void refreshFromDisk() { + refs.clearCache(); + } + + /** + * @return a representation of the index associated with this repo + * @throws IOException + */ + public GitIndex getIndex() throws IOException { + if (index == null) { + index = new GitIndex(this); + index.read(); + } else { + index.rereadIfNecessary(); + } + return index; + } + + static byte[] gitInternalSlash(byte[] bytes) { + if (File.separatorChar == '/') + return bytes; + for (int i=0; i<bytes.length; ++i) + if (bytes[i] == File.separatorChar) + bytes[i] = '/'; + return bytes; + } + + /** + * @return an important state + */ + public RepositoryState getRepositoryState() { + // Pre Git-1.6 logic + if (new File(getWorkDir(), ".dotest").exists()) + return RepositoryState.REBASING; + if (new File(gitDir,".dotest-merge").exists()) + return RepositoryState.REBASING_INTERACTIVE; + + // From 1.6 onwards + if (new File(getDirectory(),"rebase-apply/rebasing").exists()) + return RepositoryState.REBASING_REBASING; + if (new File(getDirectory(),"rebase-apply/applying").exists()) + return RepositoryState.APPLY; + if (new File(getDirectory(),"rebase-apply").exists()) + return RepositoryState.REBASING; + + if (new File(getDirectory(),"rebase-merge/interactive").exists()) + return RepositoryState.REBASING_INTERACTIVE; + if (new File(getDirectory(),"rebase-merge").exists()) + return RepositoryState.REBASING_MERGE; + + // Both versions + if (new File(gitDir,"MERGE_HEAD").exists()) + return RepositoryState.MERGING; + if (new File(gitDir,"BISECT_LOG").exists()) + return RepositoryState.BISECTING; + + return RepositoryState.SAFE; + } + + /** + * Check validity of a ref name. It must not contain character that has + * a special meaning in a Git object reference expression. Some other + * dangerous characters are also excluded. + * + * For portability reasons '\' is excluded + * + * @param refName + * + * @return true if refName is a valid ref name + */ + public static boolean isValidRefName(final String refName) { + final int len = refName.length(); + if (len == 0) + return false; + if (refName.endsWith(".lock")) + return false; + + int components = 1; + char p = '\0'; + for (int i = 0; i < len; i++) { + final char c = refName.charAt(i); + if (c <= ' ') + return false; + switch (c) { + case '.': + switch (p) { + case '\0': case '/': case '.': + return false; + } + if (i == len -1) + return false; + break; + case '/': + if (i == 0 || i == len - 1) + return false; + components++; + break; + case '{': + if (p == '@') + return false; + break; + case '~': case '^': case ':': + case '?': case '[': case '*': + case '\\': + return false; + } + p = c; + } + return components > 1; + } + + /** + * Strip work dir and return normalized repository path. + * + * @param workDir Work dir + * @param file File whose path shall be stripped of its workdir + * @return normalized repository relative path or the empty + * string if the file is not relative to the work directory. + */ + public static String stripWorkDir(File workDir, File file) { + final String filePath = file.getPath(); + final String workDirPath = workDir.getPath(); + + if (filePath.length() <= workDirPath.length() || + filePath.charAt(workDirPath.length()) != File.separatorChar || + !filePath.startsWith(workDirPath)) { + File absWd = workDir.isAbsolute() ? workDir : workDir.getAbsoluteFile(); + File absFile = file.isAbsolute() ? file : file.getAbsoluteFile(); + if (absWd == workDir && absFile == file) + return ""; + return stripWorkDir(absWd, absFile); + } + + String relName = filePath.substring(workDirPath.length() + 1); + if (File.separatorChar != '/') + relName = relName.replace(File.separatorChar, '/'); + return relName; + } + + /** + * @return the workdir file, i.e. where the files are checked out + */ + public File getWorkDir() { + return getDirectory().getParentFile(); + } + + /** + * Register a {@link RepositoryListener} which will be notified + * when ref changes are detected. + * + * @param l + */ + public void addRepositoryChangedListener(final RepositoryListener l) { + listeners.add(l); + } + + /** + * Remove a registered {@link RepositoryListener} + * @param l + */ + public void removeRepositoryChangedListener(final RepositoryListener l) { + listeners.remove(l); + } + + /** + * Register a global {@link RepositoryListener} which will be notified + * when a ref changes in any repository are detected. + * + * @param l + */ + public static void addAnyRepositoryChangedListener(final RepositoryListener l) { + allListeners.add(l); + } + + /** + * Remove a globally registered {@link RepositoryListener} + * @param l + */ + public static void removeAnyRepositoryChangedListener(final RepositoryListener l) { + allListeners.remove(l); + } + + void fireRefsMaybeChanged() { + if (refs.lastRefModification != refs.lastNotifiedRefModification) { + refs.lastNotifiedRefModification = refs.lastRefModification; + final RefsChangedEvent event = new RefsChangedEvent(this); + List<RepositoryListener> all; + synchronized (listeners) { + all = new ArrayList<RepositoryListener>(listeners); + } + synchronized (allListeners) { + all.addAll(allListeners); + } + for (final RepositoryListener l : all) { + l.refsChanged(event); + } + } + } + + void fireIndexChanged() { + final IndexChangedEvent event = new IndexChangedEvent(this); + List<RepositoryListener> all; + synchronized (listeners) { + all = new ArrayList<RepositoryListener>(listeners); + } + synchronized (allListeners) { + all.addAll(allListeners); + } + for (final RepositoryListener l : all) { + l.indexChanged(event); + } + } + + /** + * Force a scan for changed refs. + * + * @throws IOException + */ + public void scanForRepoChanges() throws IOException { + getAllRefs(); // This will look for changes to refs + getIndex(); // This will detect changes in the index + } + + /** + * @param refName + * + * @return a more user friendly ref name + */ + public String shortenRefName(String refName) { + if (refName.startsWith(Constants.R_HEADS)) + return refName.substring(Constants.R_HEADS.length()); + if (refName.startsWith(Constants.R_TAGS)) + return refName.substring(Constants.R_TAGS.length()); + if (refName.startsWith(Constants.R_REMOTES)) + return refName.substring(Constants.R_REMOTES.length()); + return refName; + } + + /** + * @param refName + * @return a {@link ReflogReader} for the supplied refname, or null if the + * named ref does not exist. + * @throws IOException the ref could not be accessed. + */ + public ReflogReader getReflogReader(String refName) throws IOException { + Ref ref = getRef(refName); + if (ref != null) + return new ReflogReader(this, ref.getOrigName()); + return null; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryAdapter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryAdapter.java new file mode 100644 index 0000000000..e43c33ad7d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryAdapter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +/** + * A default {@link RepositoryListener} that does nothing except invoke an + * optional general method for any repository change. + */ +public class RepositoryAdapter implements RepositoryListener { + + public void indexChanged(final IndexChangedEvent e) { + // Empty + } + + public void refsChanged(final RefsChangedEvent e) { + // Empty + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java new file mode 100644 index 0000000000..e8630a3c6d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.RawParseUtils; + +/** Cache of active {@link Repository} instances. */ +public class RepositoryCache { + private static final RepositoryCache cache = new RepositoryCache(); + + /** + * Open an existing repository, reusing a cached instance if possible. + * <p> + * When done with the repository, the caller must call + * {@link Repository#close()} to decrement the repository's usage counter. + * + * @param location + * where the local repository is. Typically a {@link FileKey}. + * @return the repository instance requested; caller must close when done. + * @throws IOException + * the repository could not be read (likely its core.version + * property is not supported). + * @throws RepositoryNotFoundException + * there is no repository at the given location. + */ + public static Repository open(final Key location) throws IOException, + RepositoryNotFoundException { + return open(location, true); + } + + /** + * Open a repository, reusing a cached instance if possible. + * <p> + * When done with the repository, the caller must call + * {@link Repository#close()} to decrement the repository's usage counter. + * + * @param location + * where the local repository is. Typically a {@link FileKey}. + * @param mustExist + * If true, and the repository is not found, throws {@code + * RepositoryNotFoundException}. If false, a repository instance + * is created and registered anyway. + * @return the repository instance requested; caller must close when done. + * @throws IOException + * the repository could not be read (likely its core.version + * property is not supported). + * @throws RepositoryNotFoundException + * There is no repository at the given location, only thrown if + * {@code mustExist} is true. + */ + public static Repository open(final Key location, final boolean mustExist) + throws IOException { + return cache.openRepository(location, mustExist); + } + + /** + * Register one repository into the cache. + * <p> + * During registration the cache automatically increments the usage counter, + * permitting it to retain the reference. A {@link FileKey} for the + * repository's {@link Repository#getDirectory()} is used to index the + * repository in the cache. + * <p> + * If another repository already is registered in the cache at this + * location, the other instance is closed. + * + * @param db + * repository to register. + */ + public static void register(final Repository db) { + cache.registerRepository(FileKey.exact(db.getDirectory()), db); + } + + /** + * Remove a repository from the cache. + * <p> + * Removes a repository from the cache, if it is still registered here, + * permitting it to close. + * + * @param db + * repository to unregister. + */ + public static void close(final Repository db) { + cache.unregisterRepository(FileKey.exact(db.getDirectory())); + } + + /** Unregister all repositories from the cache. */ + public static void clear() { + cache.clearAll(); + } + + private final ConcurrentHashMap<Key, Reference<Repository>> cacheMap; + + private final Lock[] openLocks; + + private RepositoryCache() { + cacheMap = new ConcurrentHashMap<Key, Reference<Repository>>(); + openLocks = new Lock[4]; + for (int i = 0; i < openLocks.length; i++) + openLocks[i] = new Lock(); + } + + private Repository openRepository(final Key location, + final boolean mustExist) throws IOException { + Reference<Repository> ref = cacheMap.get(location); + Repository db = ref != null ? ref.get() : null; + if (db == null) { + synchronized (lockFor(location)) { + ref = cacheMap.get(location); + db = ref != null ? ref.get() : null; + if (db == null) { + db = location.open(mustExist); + ref = new SoftReference<Repository>(db); + cacheMap.put(location, ref); + } + } + } + db.incrementOpen(); + return db; + } + + private void registerRepository(final Key location, final Repository db) { + db.incrementOpen(); + SoftReference<Repository> newRef = new SoftReference<Repository>(db); + Reference<Repository> oldRef = cacheMap.put(location, newRef); + Repository oldDb = oldRef != null ? oldRef.get() : null; + if (oldDb != null) + oldDb.close(); + } + + private void unregisterRepository(final Key location) { + Reference<Repository> oldRef = cacheMap.remove(location); + Repository oldDb = oldRef != null ? oldRef.get() : null; + if (oldDb != null) + oldDb.close(); + } + + private void clearAll() { + for (int stage = 0; stage < 2; stage++) { + for (Iterator<Map.Entry<Key, Reference<Repository>>> i = cacheMap + .entrySet().iterator(); i.hasNext();) { + final Map.Entry<Key, Reference<Repository>> e = i.next(); + final Repository db = e.getValue().get(); + if (db != null) + db.close(); + i.remove(); + } + } + } + + private Lock lockFor(final Key location) { + return openLocks[(location.hashCode() >>> 1) % openLocks.length]; + } + + private static class Lock { + // Used only for its monitor. + } + + /** + * Abstract hash key for {@link RepositoryCache} entries. + * <p> + * A Key instance should be lightweight, and implement hashCode() and + * equals() such that two Key instances are equal if they represent the same + * Repository location. + */ + public static interface Key { + /** + * Called by {@link RepositoryCache#open(Key)} if it doesn't exist yet. + * <p> + * If a repository does not exist yet in the cache, the cache will call + * this method to acquire a handle to it. + * + * @param mustExist + * true if the repository must exist in order to be opened; + * false if a new non-existent repository is permitted to be + * created (the caller is responsible for calling create). + * @return the new repository instance. + * @throws IOException + * the repository could not be read (likely its core.version + * property is not supported). + * @throws RepositoryNotFoundException + * There is no repository at the given location, only thrown + * if {@code mustExist} is true. + */ + Repository open(boolean mustExist) throws IOException, + RepositoryNotFoundException; + } + + /** Location of a Repository, using the standard java.io.File API. */ + public static class FileKey implements Key { + /** + * Obtain a pointer to an exact location on disk. + * <p> + * No guessing is performed, the given location is exactly the GIT_DIR + * directory of the repository. + * + * @param directory + * location where the repository database is. + * @return a key for the given directory. + * @see #lenient(File) + */ + public static FileKey exact(final File directory) { + return new FileKey(directory); + } + + /** + * Obtain a pointer to a location on disk. + * <p> + * The method performs some basic guessing to locate the repository. + * Searched paths are: + * <ol> + * <li>{@code directory} // assume exact match</li> + * <li>{@code directory} + "/.git" // assume working directory</li> + * <li>{@code directory} + ".git" // assume bare</li> + * </ol> + * + * @param directory + * location where the repository database might be. + * @return a key for the given directory. + * @see #exact(File) + */ + public static FileKey lenient(final File directory) { + final File gitdir = resolve(directory); + return new FileKey(gitdir != null ? gitdir : directory); + } + + private final File path; + + /** + * @param directory + * exact location of the repository. + */ + protected FileKey(final File directory) { + path = canonical(directory); + } + + private static File canonical(final File path) { + try { + return path.getCanonicalFile(); + } catch (IOException e) { + return path.getAbsoluteFile(); + } + } + + /** @return location supplied to the constructor. */ + public final File getFile() { + return path; + } + + public Repository open(final boolean mustExist) throws IOException { + if (mustExist && !isGitRepository(path)) + throw new RepositoryNotFoundException(path); + return new Repository(path); + } + + @Override + public int hashCode() { + return path.hashCode(); + } + + @Override + public boolean equals(final Object o) { + return o instanceof FileKey && path.equals(((FileKey) o).path); + } + + @Override + public String toString() { + return path.toString(); + } + + /** + * Guess if a directory contains a Git repository. + * <p> + * This method guesses by looking for the existence of some key files + * and directories. + * + * @param dir + * the location of the directory to examine. + * @return true if the directory "looks like" a Git repository; false if + * it doesn't look enough like a Git directory to really be a + * Git directory. + */ + public static boolean isGitRepository(final File dir) { + return FS.resolve(dir, "objects").exists() + && FS.resolve(dir, "refs").exists() + && isValidHead(new File(dir, Constants.HEAD)); + } + + private static boolean isValidHead(final File head) { + final String ref = readFirstLine(head); + return ref != null + && (ref.startsWith("ref: refs/") || ObjectId.isId(ref)); + } + + private static String readFirstLine(final File head) { + try { + final byte[] buf = NB.readFully(head, 4096); + int n = buf.length; + if (n == 0) + return null; + if (buf[n - 1] == '\n') + n--; + return RawParseUtils.decode(buf, 0, n); + } catch (IOException e) { + return null; + } + } + + /** + * Guess the proper path for a Git repository. + * <p> + * The method performs some basic guessing to locate the repository. + * Searched paths are: + * <ol> + * <li>{@code directory} // assume exact match</li> + * <li>{@code directory} + "/.git" // assume working directory</li> + * <li>{@code directory} + ".git" // assume bare</li> + * </ol> + * + * @param directory + * location to guess from. Several permutations are tried. + * @return the actual directory location if a better match is found; + * null if there is no suitable match. + */ + public static File resolve(final File directory) { + if (isGitRepository(directory)) + return directory; + if (isGitRepository(new File(directory, ".git"))) + return new File(directory, ".git"); + + final String name = directory.getName(); + final File parent = directory.getParentFile(); + if (isGitRepository(new File(parent, name + ".git"))) + return new File(parent, name + ".git"); + return null; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryChangedEvent.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryChangedEvent.java new file mode 100644 index 0000000000..495049ce74 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryChangedEvent.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +/** + * This class passes information about changed refs to a + * {@link RepositoryListener} + * + * Currently only a reference to the repository is passed. + */ +public class RepositoryChangedEvent { + private final Repository repository; + + RepositoryChangedEvent(final Repository repository) { + this.repository = repository; + } + + /** + * @return the repository that was changed + */ + public Repository getRepository() { + return repository; + } + + @Override + public String toString() { + return "RepositoryChangedEvent[" + repository + "]"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryConfig.java new file mode 100644 index 0000000000..805975a8d1 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryConfig.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2009, JetBrains s.r.o. + * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * Copyright (C) 2008, Thad Hughes <thadh@thad.corp.google.com> + * Copyright (C) 2009, Yann Simon <yann.simon.fr@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; + +/** + * An object representing the Git config file. + * + * This can be either the repository specific file or the user global + * file depending on how it is instantiated. + */ +public class RepositoryConfig extends FileBasedConfig { + /** Section name for a branch configuration. */ + public static final String BRANCH_SECTION = "branch"; + + /** + * Create a Git configuration file reader/writer/cache for a specific file. + * + * @param base + * configuration that provides default values if this file does + * not set/override a particular key. Often this is the user's + * global configuration file, or the system level configuration. + * @param cfgLocation + * path of the file to load (or save). + */ + public RepositoryConfig(final Config base, final File cfgLocation) { + super(base, cfgLocation); + } + + /** + * @return Core configuration values + */ + public CoreConfig getCore() { + return get(CoreConfig.KEY); + } + + /** + * @return transfer, fetch and receive configuration values + */ + public TransferConfig getTransfer() { + return get(TransferConfig.KEY); + } + + /** @return standard user configuration data */ + public UserConfig getUserConfig() { + return get(UserConfig.KEY); + } + + /** + * @return the author name as defined in the git variables + * and configurations. If no name could be found, try + * to use the system user name instead. + */ + public String getAuthorName() { + return getUserConfig().getAuthorName(); + } + + /** + * @return the committer name as defined in the git variables + * and configurations. If no name could be found, try + * to use the system user name instead. + */ + public String getCommitterName() { + return getUserConfig().getCommitterName(); + } + + /** + * @return the author email as defined in git variables and + * configurations. If no email could be found, try to + * propose one default with the user name and the + * host name. + */ + public String getAuthorEmail() { + return getUserConfig().getAuthorEmail(); + } + + /** + * @return the committer email as defined in git variables and + * configurations. If no email could be found, try to + * propose one default with the user name and the + * host name. + */ + public String getCommitterEmail() { + return getUserConfig().getCommitterEmail(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryListener.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryListener.java new file mode 100644 index 0000000000..0473093e20 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryListener.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +/** + * A RepositoryListener gets notification about changes in refs or repository. + * <p> + * It currently does <em>not</em> get notification about which items are + * changed. + */ +public interface RepositoryListener { + /** + * Invoked when a ref changes + * + * @param e + * information about the changes. + */ + void refsChanged(RefsChangedEvent e); + + /** + * Invoked when the index changes + * + * @param e + * information about the changes. + */ + void indexChanged(IndexChangedEvent e); + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java new file mode 100644 index 0000000000..6159839b13 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryState.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2008, Mike Ralphson <mike@abacus.co.uk> + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg.lists@dewire.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +/** + * Important state of the repository that affects what can and cannot bed + * done. This is things like unhandled conflicted merges and unfinished rebase. + * + * The granularity and set of states are somewhat arbitrary. The methods + * on the state are the only supported means of deciding what to do. + */ +public enum RepositoryState { + /** + * A safe state for working normally + * */ + SAFE { + public boolean canCheckout() { return true; } + public boolean canResetHead() { return true; } + public boolean canCommit() { return true; } + public String getDescription() { return "Normal"; } + }, + + /** An unfinished merge. Must resole or reset before continuing normally + */ + MERGING { + public boolean canCheckout() { return false; } + public boolean canResetHead() { return false; } + public boolean canCommit() { return false; } + public String getDescription() { return "Conflicts"; } + }, + + /** + * An unfinished rebase or am. Must resolve, skip or abort before normal work can take place + */ + REBASING { + public boolean canCheckout() { return false; } + public boolean canResetHead() { return false; } + public boolean canCommit() { return true; } + public String getDescription() { return "Rebase/Apply mailbox"; } + }, + + /** + * An unfinished rebase. Must resolve, skip or abort before normal work can take place + */ + REBASING_REBASING { + public boolean canCheckout() { return false; } + public boolean canResetHead() { return false; } + public boolean canCommit() { return true; } + public String getDescription() { return "Rebase"; } + }, + + /** + * An unfinished apply. Must resolve, skip or abort before normal work can take place + */ + APPLY { + public boolean canCheckout() { return false; } + public boolean canResetHead() { return false; } + public boolean canCommit() { return true; } + public String getDescription() { return "Apply mailbox"; } + }, + + /** + * An unfinished rebase with merge. Must resolve, skip or abort before normal work can take place + */ + REBASING_MERGE { + public boolean canCheckout() { return false; } + public boolean canResetHead() { return false; } + public boolean canCommit() { return true; } + public String getDescription() { return "Rebase w/merge"; } + }, + + /** + * An unfinished interactive rebase. Must resolve, skip or abort before normal work can take place + */ + REBASING_INTERACTIVE { + public boolean canCheckout() { return false; } + public boolean canResetHead() { return false; } + public boolean canCommit() { return true; } + public String getDescription() { return "Rebase interactive"; } + }, + + /** + * Bisecting being done. Normal work may continue but is discouraged + */ + BISECTING { + /* Changing head is a normal operation when bisecting */ + public boolean canCheckout() { return true; } + + /* Do not reset, checkout instead */ + public boolean canResetHead() { return false; } + + /* Actually it may make sense, but for now we err on the side of caution */ + public boolean canCommit() { return false; } + + public String getDescription() { return "Bisecting"; } + }; + + /** + * @return true if changing HEAD is sane. + */ + public abstract boolean canCheckout(); + + /** + * @return true if we can commit + */ + public abstract boolean canCommit(); + + /** + * @return true if reset to another HEAD is considered SAFE + */ + public abstract boolean canResetHead(); + + /** + * @return a human readable description of the state. + */ + public abstract String getDescription(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java new file mode 100644 index 0000000000..81666be45d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; + +/** + * A tree entry representing a symbolic link. + * + * Note. Java cannot really handle these as file system objects. + */ +public class SymlinkTreeEntry extends TreeEntry { + private static final long serialVersionUID = 1L; + + /** + * Construct a {@link SymlinkTreeEntry} with the specified name and SHA-1 in + * the specified parent + * + * @param parent + * @param id + * @param nameUTF8 + */ + public SymlinkTreeEntry(final Tree parent, final ObjectId id, + final byte[] nameUTF8) { + super(parent, id, nameUTF8); + } + + public FileMode getMode() { + return FileMode.SYMLINK; + } + + public void accept(final TreeVisitor tv, final int flags) + throws IOException { + if ((MODIFIED_ONLY & flags) == MODIFIED_ONLY && !isModified()) { + return; + } + + tv.visitSymlink(this); + } + + public String toString() { + final StringBuffer r = new StringBuffer(); + r.append(ObjectId.toString(getId())); + r.append(" S "); + r.append(getFullName()); + return r.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tag.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tag.java new file mode 100644 index 0000000000..0e1c1651de --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tag.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2006-2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.ObjectWritingException; + +/** + * Represents a named reference to another Git object of any type. + */ +public class Tag { + private final Repository objdb; + + private ObjectId tagId; + + private PersonIdent tagger; + + private String message; + + private byte[] raw; + + private String type; + + private String tag; + + private ObjectId objId; + + /** + * Construct a new, yet unnamed Tag. + * + * @param db + */ + public Tag(final Repository db) { + objdb = db; + } + + /** + * Construct a Tag representing an existing with a known name referencing an known object. + * This could be either a simple or annotated tag. + * + * @param db {@link Repository} + * @param id target id. + * @param refName tag name or null + * @param raw data of an annotated tag. + */ + public Tag(final Repository db, final ObjectId id, String refName, final byte[] raw) { + objdb = db; + if (raw != null) { + tagId = id; + objId = ObjectId.fromString(raw, 7); + } else + objId = id; + if (refName != null && refName.startsWith("refs/tags/")) + refName = refName.substring(10); + tag = refName; + this.raw = raw; + } + + /** + * @return tagger of a annotated tag or null + */ + public PersonIdent getAuthor() { + decode(); + return tagger; + } + + /** + * Set author of an annotated tag. + * @param a author identifier as a {@link PersonIdent} + */ + public void setAuthor(final PersonIdent a) { + tagger = a; + } + + /** + * @return comment of an annotated tag, or null + */ + public String getMessage() { + decode(); + return message; + } + + private void decode() { + // FIXME: handle I/O errors + if (raw != null) { + try { + BufferedReader br = new BufferedReader(new InputStreamReader( + new ByteArrayInputStream(raw))); + String n = br.readLine(); + if (n == null || !n.startsWith("object ")) { + throw new CorruptObjectException(tagId, "no object"); + } + objId = ObjectId.fromString(n.substring(7)); + n = br.readLine(); + if (n == null || !n.startsWith("type ")) { + throw new CorruptObjectException(tagId, "no type"); + } + type = n.substring("type ".length()); + n = br.readLine(); + + if (n == null || !n.startsWith("tag ")) { + throw new CorruptObjectException(tagId, "no tag name"); + } + tag = n.substring("tag ".length()); + n = br.readLine(); + + // We should see a "tagger" header here, but some repos have tags + // without it. + if (n == null) + throw new CorruptObjectException(tagId, "no tagger header"); + + if (n.length()>0) + if (n.startsWith("tagger ")) + tagger = new PersonIdent(n.substring("tagger ".length())); + else + throw new CorruptObjectException(tagId, "no tagger/bad header"); + + // Message should start with an empty line, but + StringBuffer tempMessage = new StringBuffer(); + char[] readBuf = new char[2048]; + int readLen; + while ((readLen = br.read(readBuf)) > 0) { + tempMessage.append(readBuf, 0, readLen); + } + message = tempMessage.toString(); + if (message.startsWith("\n")) + message = message.substring(1); + } catch (IOException e) { + e.printStackTrace(); + } finally { + raw = null; + } + } + } + + /** + * Set the message of an annotated tag + * @param m + */ + public void setMessage(final String m) { + message = m; + } + + /** + * Store a tag. + * If author, message or type is set make the tag an annotated tag. + * + * @throws IOException + */ + public void tag() throws IOException { + if (getTagId() != null) + throw new IllegalStateException("exists " + getTagId()); + final ObjectId id; + final RefUpdate ru; + + if (tagger!=null || message!=null || type!=null) { + ObjectId tagid = new ObjectWriter(objdb).writeTag(this); + setTagId(tagid); + id = tagid; + } else { + id = objId; + } + + ru = objdb.updateRef(Constants.R_TAGS + getTag()); + ru.setNewObjectId(id); + ru.setRefLogMessage("tagged " + getTag(), false); + if (ru.forceUpdate() == RefUpdate.Result.LOCK_FAILURE) + throw new ObjectWritingException("Unable to lock tag " + getTag()); + } + + public String toString() { + return "tag[" + getTag() + getType() + getObjId() + " " + getAuthor() + "]"; + } + + /** + * @return SHA-1 of this tag (if annotated and stored). + */ + public ObjectId getTagId() { + return tagId; + } + + /** + * Set SHA-1 of this tag. Used by writer. + * + * @param tagId + */ + public void setTagId(ObjectId tagId) { + this.tagId = tagId; + } + + /** + * @return creator of this tag. + */ + public PersonIdent getTagger() { + decode(); + return tagger; + } + + /** + * Set the creator of this tag + * + * @param tagger + */ + public void setTagger(PersonIdent tagger) { + this.tagger = tagger; + } + + /** + * @return tag target type + */ + public String getType() { + decode(); + return type; + } + + /** + * Set tag target type + * @param type + */ + public void setType(String type) { + this.type = type; + } + + /** + * @return name of the tag. + */ + public String getTag() { + return tag; + } + + /** + * Set the name of this tag. + * + * @param tag + */ + public void setTag(String tag) { + this.tag = tag; + } + + /** + * @return the SHA'1 of the object this tag refers to. + */ + public ObjectId getObjId() { + return objId; + } + + /** + * Set the id of the object this tag refers to. + * + * @param objId + */ + public void setObjId(ObjectId objId) { + this.objId = objId; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java new file mode 100644 index 0000000000..a668b11be8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +/** + * A simple progress reporter printing on stderr + */ +public class TextProgressMonitor implements ProgressMonitor { + private boolean output; + + private long taskBeganAt; + + private String msg; + + private int lastWorked; + + private int totalWork; + + /** Initialize a new progress monitor. */ + public TextProgressMonitor() { + taskBeganAt = System.currentTimeMillis(); + } + + public void start(final int totalTasks) { + // Ignore the number of tasks. + taskBeganAt = System.currentTimeMillis(); + } + + public void beginTask(final String title, final int total) { + endTask(); + msg = title; + lastWorked = 0; + totalWork = total; + } + + public void update(final int completed) { + if (msg == null) + return; + + final int cmp = lastWorked + completed; + if (!output && System.currentTimeMillis() - taskBeganAt < 500) + return; + if (totalWork == UNKNOWN) { + display(cmp); + System.err.flush(); + } else { + if ((cmp * 100 / totalWork) != (lastWorked * 100) / totalWork) { + display(cmp); + System.err.flush(); + } + } + lastWorked = cmp; + output = true; + } + + private void display(final int cmp) { + final StringBuilder m = new StringBuilder(); + m.append('\r'); + m.append(msg); + m.append(": "); + while (m.length() < 25) + m.append(' '); + + if (totalWork == UNKNOWN) { + m.append(cmp); + } else { + final String twstr = String.valueOf(totalWork); + String cmpstr = String.valueOf(cmp); + while (cmpstr.length() < twstr.length()) + cmpstr = " " + cmpstr; + final int pcnt = (cmp * 100 / totalWork); + if (pcnt < 100) + m.append(' '); + if (pcnt < 10) + m.append(' '); + m.append(pcnt); + m.append("% ("); + m.append(cmpstr); + m.append("/"); + m.append(twstr); + m.append(")"); + } + + System.err.print(m); + } + + public boolean isCancelled() { + return false; + } + + public void endTask() { + if (output) { + if (totalWork != UNKNOWN) + display(totalWork); + System.err.println(); + } + output = false; + msg = null; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TransferConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TransferConfig.java new file mode 100644 index 0000000000..a745cbecdf --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TransferConfig.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import org.eclipse.jgit.lib.Config.SectionParser; + +/** + * The standard "transfer", "fetch" and "receive" configuration parameters. + */ +public class TransferConfig { + /** Key for {@link Config#get(SectionParser)}. */ + public static final Config.SectionParser<TransferConfig> KEY = new SectionParser<TransferConfig>() { + public TransferConfig parse(final Config cfg) { + return new TransferConfig(cfg); + } + }; + + private final boolean fsckObjects; + + private TransferConfig(final Config rc) { + fsckObjects = rc.getBoolean("receive", "fsckobjects", false); + } + + /** + * @return strictly verify received objects? + */ + public boolean isFsckObjects() { + return fsckObjects; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java new file mode 100644 index 0000000000..2b8a0e7cf3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java @@ -0,0 +1,608 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <me@lathund.dewire.com> + * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.EntryExistsException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * A representation of a Git tree entry. A Tree is a directory in Git. + */ +public class Tree extends TreeEntry implements Treeish { + private static final TreeEntry[] EMPTY_TREE = {}; + + /** + * Compare two names represented as bytes. Since git treats names of trees and + * blobs differently we have one parameter that represents a '/' for trees. For + * other objects the value should be NUL. The names are compare by their positive + * byte value (0..255). + * + * A blob and a tree with the same name will not compare equal. + * + * @param a name + * @param b name + * @param lasta '/' if a is a tree, else NUL + * @param lastb '/' if b is a tree, else NUL + * + * @return < 0 if a is sorted before b, 0 if they are the same, else b + */ + public static final int compareNames(final byte[] a, final byte[] b, final int lasta,final int lastb) { + return compareNames(a, b, 0, b.length, lasta, lastb); + } + + private static final int compareNames(final byte[] a, final byte[] nameUTF8, + final int nameStart, final int nameEnd, final int lasta, int lastb) { + int j,k; + for (j = 0, k = nameStart; j < a.length && k < nameEnd; j++, k++) { + final int aj = a[j] & 0xff; + final int bk = nameUTF8[k] & 0xff; + if (aj < bk) + return -1; + else if (aj > bk) + return 1; + } + if (j < a.length) { + int aj = a[j]&0xff; + if (aj < lastb) + return -1; + else if (aj > lastb) + return 1; + else + if (j == a.length - 1) + return 0; + else + return -1; + } + if (k < nameEnd) { + int bk = nameUTF8[k] & 0xff; + if (lasta < bk) + return -1; + else if (lasta > bk) + return 1; + else + if (k == nameEnd - 1) + return 0; + else + return 1; + } + if (lasta < lastb) + return -1; + else if (lasta > lastb) + return 1; + + final int namelength = nameEnd - nameStart; + if (a.length == namelength) + return 0; + else if (a.length < namelength) + return -1; + else + return 1; + } + + private static final byte[] substring(final byte[] s, final int nameStart, + final int nameEnd) { + if (nameStart == 0 && nameStart == s.length) + return s; + final byte[] n = new byte[nameEnd - nameStart]; + System.arraycopy(s, nameStart, n, 0, n.length); + return n; + } + + private static final int binarySearch(final TreeEntry[] entries, + final byte[] nameUTF8, final int nameUTF8last, final int nameStart, final int nameEnd) { + if (entries.length == 0) + return -1; + int high = entries.length; + int low = 0; + do { + final int mid = (low + high) >>> 1; + final int cmp = compareNames(entries[mid].getNameUTF8(), nameUTF8, + nameStart, nameEnd, TreeEntry.lastChar(entries[mid]), nameUTF8last); + if (cmp < 0) + low = mid + 1; + else if (cmp == 0) + return mid; + else + high = mid; + } while (low < high); + return -(low + 1); + } + + private final Repository db; + + private TreeEntry[] contents; + + /** + * Constructor for a new Tree + * + * @param repo The repository that owns the Tree. + */ + public Tree(final Repository repo) { + super(null, null, null); + db = repo; + contents = EMPTY_TREE; + } + + /** + * Construct a Tree object with known content and hash value + * + * @param repo + * @param myId + * @param raw + * @throws IOException + */ + public Tree(final Repository repo, final ObjectId myId, final byte[] raw) + throws IOException { + super(null, myId, null); + db = repo; + readTree(raw); + } + + /** + * Construct a new Tree under another Tree + * + * @param parent + * @param nameUTF8 + */ + public Tree(final Tree parent, final byte[] nameUTF8) { + super(parent, null, nameUTF8); + db = parent.getRepository(); + contents = EMPTY_TREE; + } + + /** + * Construct a Tree with a known SHA-1 under another tree. Data is not yet + * specified and will have to be loaded on demand. + * + * @param parent + * @param id + * @param nameUTF8 + */ + public Tree(final Tree parent, final ObjectId id, final byte[] nameUTF8) { + super(parent, id, nameUTF8); + db = parent.getRepository(); + } + + public FileMode getMode() { + return FileMode.TREE; + } + + /** + * @return true if this Tree is the top level Tree. + */ + public boolean isRoot() { + return getParent() == null; + } + + public Repository getRepository() { + return db; + } + + public final ObjectId getTreeId() { + return getId(); + } + + public final Tree getTree() { + return this; + } + + /** + * @return true of the data of this Tree is loaded + */ + public boolean isLoaded() { + return contents != null; + } + + /** + * Forget the in-memory data for this tree. + */ + public void unload() { + if (isModified()) + throw new IllegalStateException("Cannot unload a modified tree."); + contents = null; + } + + /** + * Adds a new or existing file with the specified name to this tree. + * Trees are added if necessary as the name may contain '/':s. + * + * @param name Name + * @return a {@link FileTreeEntry} for the added file. + * @throws IOException + */ + public FileTreeEntry addFile(final String name) throws IOException { + return addFile(Repository.gitInternalSlash(Constants.encode(name)), 0); + } + + /** + * Adds a new or existing file with the specified name to this tree. + * Trees are added if necessary as the name may contain '/':s. + * + * @param s an array containing the name + * @param offset when the name starts in the tree. + * + * @return a {@link FileTreeEntry} for the added file. + * @throws IOException + */ + public FileTreeEntry addFile(final byte[] s, final int offset) + throws IOException { + int slash; + int p; + + for (slash = offset; slash < s.length && s[slash] != '/'; slash++) { + // search for path component terminator + } + + ensureLoaded(); + byte xlast = slash<s.length ? (byte)'/' : 0; + p = binarySearch(contents, s, xlast, offset, slash); + if (p >= 0 && slash < s.length && contents[p] instanceof Tree) + return ((Tree) contents[p]).addFile(s, slash + 1); + + final byte[] newName = substring(s, offset, slash); + if (p >= 0) + throw new EntryExistsException(RawParseUtils.decode(newName)); + else if (slash < s.length) { + final Tree t = new Tree(this, newName); + insertEntry(p, t); + return t.addFile(s, slash + 1); + } else { + final FileTreeEntry f = new FileTreeEntry(this, null, newName, + false); + insertEntry(p, f); + return f; + } + } + + /** + * Adds a new or existing Tree with the specified name to this tree. + * Trees are added if necessary as the name may contain '/':s. + * + * @param name Name + * @return a {@link FileTreeEntry} for the added tree. + * @throws IOException + */ + public Tree addTree(final String name) throws IOException { + return addTree(Repository.gitInternalSlash(Constants.encode(name)), 0); + } + + /** + * Adds a new or existing Tree with the specified name to this tree. + * Trees are added if necessary as the name may contain '/':s. + * + * @param s an array containing the name + * @param offset when the name starts in the tree. + * + * @return a {@link FileTreeEntry} for the added tree. + * @throws IOException + */ + public Tree addTree(final byte[] s, final int offset) throws IOException { + int slash; + int p; + + for (slash = offset; slash < s.length && s[slash] != '/'; slash++) { + // search for path component terminator + } + + ensureLoaded(); + p = binarySearch(contents, s, (byte)'/', offset, slash); + if (p >= 0 && slash < s.length && contents[p] instanceof Tree) + return ((Tree) contents[p]).addTree(s, slash + 1); + + final byte[] newName = substring(s, offset, slash); + if (p >= 0) + throw new EntryExistsException(RawParseUtils.decode(newName)); + + final Tree t = new Tree(this, newName); + insertEntry(p, t); + return slash == s.length ? t : t.addTree(s, slash + 1); + } + + /** + * Add the specified tree entry to this tree. + * + * @param e + * @throws IOException + */ + public void addEntry(final TreeEntry e) throws IOException { + final int p; + + ensureLoaded(); + p = binarySearch(contents, e.getNameUTF8(), TreeEntry.lastChar(e), 0, e.getNameUTF8().length); + if (p < 0) { + e.attachParent(this); + insertEntry(p, e); + } else { + throw new EntryExistsException(e.getName()); + } + } + + private void insertEntry(int p, final TreeEntry e) { + final TreeEntry[] c = contents; + final TreeEntry[] n = new TreeEntry[c.length + 1]; + p = -(p + 1); + for (int k = c.length - 1; k >= p; k--) + n[k + 1] = c[k]; + n[p] = e; + for (int k = p - 1; k >= 0; k--) + n[k] = c[k]; + contents = n; + setModified(); + } + + void removeEntry(final TreeEntry e) { + final TreeEntry[] c = contents; + final int p = binarySearch(c, e.getNameUTF8(), TreeEntry.lastChar(e), 0, + e.getNameUTF8().length); + if (p >= 0) { + final TreeEntry[] n = new TreeEntry[c.length - 1]; + for (int k = c.length - 1; k > p; k--) + n[k - 1] = c[k]; + for (int k = p - 1; k >= 0; k--) + n[k] = c[k]; + contents = n; + setModified(); + } + } + + /** + * @return number of members in this tree + * @throws IOException + */ + public int memberCount() throws IOException { + ensureLoaded(); + return contents.length; + } + + /** + * Return all members of the tree sorted in Git order. + * + * Entries are sorted by the numerical unsigned byte + * values with (sub)trees having an implicit '/'. An + * example of a tree with three entries. a:b is an + * actual file name here. + * + * <p> + * 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a.b + * 040000 tree 4277b6e69d25e5efa77c455340557b384a4c018a a + * 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a:b + * + * @return all entries in this Tree, sorted. + * @throws IOException + */ + public TreeEntry[] members() throws IOException { + ensureLoaded(); + final TreeEntry[] c = contents; + if (c.length != 0) { + final TreeEntry[] r = new TreeEntry[c.length]; + for (int k = c.length - 1; k >= 0; k--) + r[k] = c[k]; + return r; + } else + return c; + } + + private boolean exists(final String s, byte slast) throws IOException { + return findMember(s, slast) != null; + } + + /** + * @param path to the tree. + * @return true if a tree with the specified path can be found under this + * tree. + * @throws IOException + */ + public boolean existsTree(String path) throws IOException { + return exists(path,(byte)'/'); + } + + /** + * @param path of the non-tree entry. + * @return true if a blob, symlink, or gitlink with the specified name + * can be found under this tree. + * @throws IOException + */ + public boolean existsBlob(String path) throws IOException { + return exists(path,(byte)0); + } + + private TreeEntry findMember(final String s, byte slast) throws IOException { + return findMember(Repository.gitInternalSlash(Constants.encode(s)), slast, 0); + } + + private TreeEntry findMember(final byte[] s, final byte slast, final int offset) + throws IOException { + int slash; + int p; + + for (slash = offset; slash < s.length && s[slash] != '/'; slash++) { + // search for path component terminator + } + + ensureLoaded(); + byte xlast = slash<s.length ? (byte)'/' : slast; + p = binarySearch(contents, s, xlast, offset, slash); + if (p >= 0) { + final TreeEntry r = contents[p]; + if (slash < s.length-1) + return r instanceof Tree ? ((Tree) r).findMember(s, slast, slash + 1) + : null; + return r; + } + return null; + } + + /** + * @param s + * blob name + * @return a {@link TreeEntry} representing an object with the specified + * relative path. + * @throws IOException + */ + public TreeEntry findBlobMember(String s) throws IOException { + return findMember(s,(byte)0); + } + + /** + * @param s Tree Name + * @return a Tree with the name s or null + * @throws IOException + */ + public TreeEntry findTreeMember(String s) throws IOException { + return findMember(s,(byte)'/'); + } + + public void accept(final TreeVisitor tv, final int flags) + throws IOException { + final TreeEntry[] c; + + if ((MODIFIED_ONLY & flags) == MODIFIED_ONLY && !isModified()) + return; + + if ((LOADED_ONLY & flags) == LOADED_ONLY && !isLoaded()) { + tv.startVisitTree(this); + tv.endVisitTree(this); + return; + } + + ensureLoaded(); + tv.startVisitTree(this); + + if ((CONCURRENT_MODIFICATION & flags) == CONCURRENT_MODIFICATION) + c = members(); + else + c = contents; + + for (int k = 0; k < c.length; k++) + c[k].accept(tv, flags); + + tv.endVisitTree(this); + } + + private void ensureLoaded() throws IOException, MissingObjectException { + if (!isLoaded()) { + final ObjectLoader or = db.openTree(getId()); + if (or == null) + throw new MissingObjectException(getId(), Constants.TYPE_TREE); + readTree(or.getBytes()); + } + } + + private void readTree(final byte[] raw) throws IOException { + final int rawSize = raw.length; + int rawPtr = 0; + TreeEntry[] temp; + int nextIndex = 0; + + while (rawPtr < rawSize) { + while (rawPtr < rawSize && raw[rawPtr] != 0) + rawPtr++; + rawPtr++; + rawPtr += Constants.OBJECT_ID_LENGTH; + nextIndex++; + } + + temp = new TreeEntry[nextIndex]; + rawPtr = 0; + nextIndex = 0; + while (rawPtr < rawSize) { + int c = raw[rawPtr++]; + if (c < '0' || c > '7') + throw new CorruptObjectException(getId(), "invalid entry mode"); + int mode = c - '0'; + for (;;) { + c = raw[rawPtr++]; + if (' ' == c) + break; + else if (c < '0' || c > '7') + throw new CorruptObjectException(getId(), "invalid mode"); + mode <<= 3; + mode += c - '0'; + } + + int nameLen = 0; + while (raw[rawPtr + nameLen] != 0) + nameLen++; + final byte[] name = new byte[nameLen]; + System.arraycopy(raw, rawPtr, name, 0, nameLen); + rawPtr += nameLen + 1; + + final ObjectId id = ObjectId.fromRaw(raw, rawPtr); + rawPtr += Constants.OBJECT_ID_LENGTH; + + final TreeEntry ent; + if (FileMode.REGULAR_FILE.equals(mode)) + ent = new FileTreeEntry(this, id, name, false); + else if (FileMode.EXECUTABLE_FILE.equals(mode)) + ent = new FileTreeEntry(this, id, name, true); + else if (FileMode.TREE.equals(mode)) + ent = new Tree(this, id, name); + else if (FileMode.SYMLINK.equals(mode)) + ent = new SymlinkTreeEntry(this, id, name); + else if (FileMode.GITLINK.equals(mode)) + ent = new GitlinkTreeEntry(this, id, name); + else + throw new CorruptObjectException(getId(), "Invalid mode: " + + Integer.toOctalString(mode)); + temp[nextIndex++] = ent; + } + + contents = temp; + } + + public String toString() { + final StringBuffer r = new StringBuffer(); + r.append(ObjectId.toString(getId())); + r.append(" T "); + r.append(getFullName()); + return r.toString(); + } + +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java new file mode 100644 index 0000000000..b6dd9311ae --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; + +import org.eclipse.jgit.lib.GitIndex.Entry; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * This class represents an entry in a tree, like a blob or another tree. + */ +public abstract class TreeEntry implements Comparable { + /** + * a flag for {@link TreeEntry#accept(TreeVisitor, int)} to visit only modified entries + */ + public static final int MODIFIED_ONLY = 1 << 0; + + /** + * a flag for {@link TreeEntry#accept(TreeVisitor, int)} to visit only loaded entries + */ + public static final int LOADED_ONLY = 1 << 1; + + /** + * a flag for {@link TreeEntry#accept(TreeVisitor, int)} obsolete? + */ + public static final int CONCURRENT_MODIFICATION = 1 << 2; + + private byte[] nameUTF8; + + private Tree parent; + + private ObjectId id; + + /** + * Construct a named tree entry. + * + * @param myParent + * @param myId + * @param myNameUTF8 + */ + protected TreeEntry(final Tree myParent, final ObjectId myId, + final byte[] myNameUTF8) { + nameUTF8 = myNameUTF8; + parent = myParent; + id = myId; + } + + /** + * @return parent of this tree. + */ + public Tree getParent() { + return parent; + } + + /** + * Delete this entry. + */ + public void delete() { + getParent().removeEntry(this); + detachParent(); + } + + /** + * Detach this entry from it's parent. + */ + public void detachParent() { + parent = null; + } + + void attachParent(final Tree p) { + parent = p; + } + + /** + * @return the repository owning this entry. + */ + public Repository getRepository() { + return getParent().getRepository(); + } + + /** + * @return the raw byte name of this entry. + */ + public byte[] getNameUTF8() { + return nameUTF8; + } + + /** + * @return the name of this entry. + */ + public String getName() { + if (nameUTF8 != null) + return RawParseUtils.decode(nameUTF8); + return null; + } + + /** + * Rename this entry. + * + * @param n The new name + * @throws IOException + */ + public void rename(final String n) throws IOException { + rename(Constants.encode(n)); + } + + /** + * Rename this entry. + * + * @param n The new name + * @throws IOException + */ + public void rename(final byte[] n) throws IOException { + final Tree t = getParent(); + if (t != null) { + delete(); + } + nameUTF8 = n; + if (t != null) { + t.addEntry(this); + } + } + + /** + * @return true if this entry is new or modified since being loaded. + */ + public boolean isModified() { + return getId() == null; + } + + /** + * Mark this entry as modified. + */ + public void setModified() { + setId(null); + } + + /** + * @return SHA-1 of this tree entry (null for new unhashed entries) + */ + public ObjectId getId() { + return id; + } + + /** + * Set (update) the SHA-1 of this entry. Invalidates the id's of all + * entries above this entry as they will have to be recomputed. + * + * @param n SHA-1 for this entry. + */ + public void setId(final ObjectId n) { + // If we have a parent and our id is being cleared or changed then force + // the parent's id to become unset as it depends on our id. + // + final Tree p = getParent(); + if (p != null && id != n) { + if ((id == null && n != null) || (id != null && n == null) + || !id.equals(n)) { + p.setId(null); + } + } + + id = n; + } + + /** + * @return repository relative name of this entry + */ + public String getFullName() { + final StringBuffer r = new StringBuffer(); + appendFullName(r); + return r.toString(); + } + + /** + * @return repository relative name of the entry + * FIXME better encoding + */ + public byte[] getFullNameUTF8() { + return getFullName().getBytes(); + } + + public int compareTo(final Object o) { + if (this == o) + return 0; + if (o instanceof TreeEntry) + return Tree.compareNames(nameUTF8, ((TreeEntry) o).nameUTF8, lastChar(this), lastChar((TreeEntry)o)); + return -1; + } + + /** + * Helper for accessing tree/blob methods. + * + * @param treeEntry + * @return '/' for Tree entries and NUL for non-treeish objects. + */ + final public static int lastChar(TreeEntry treeEntry) { + if (!(treeEntry instanceof Tree)) + return '\0'; + else + return '/'; + } + + /** + * Helper for accessing tree/blob/index methods. + * + * @param i + * @return '/' for Tree entries and NUL for non-treeish objects + */ + final public static int lastChar(Entry i) { + // FIXME, gitlink etc. Currently Trees cannot appear in the + // index so '\0' is always returned, except maybe for submodules + // which we do not support yet. + return FileMode.TREE.equals(i.getModeBits()) ? '/' : '\0'; + } + + /** + * See @{link {@link #accept(TreeVisitor, int)}. + * + * @param tv + * @throws IOException + */ + public void accept(final TreeVisitor tv) throws IOException { + accept(tv, 0); + } + + /** + * Visit the members of this TreeEntry. + * + * @param tv + * A visitor object doing the work + * @param flags + * Specification for what members to visit. See + * {@link #MODIFIED_ONLY}, {@link #LOADED_ONLY}, + * {@link #CONCURRENT_MODIFICATION}. + * @throws IOException + */ + public abstract void accept(TreeVisitor tv, int flags) throws IOException; + + /** + * @return mode (type of object) + */ + public abstract FileMode getMode(); + + private void appendFullName(final StringBuffer r) { + final TreeEntry p = getParent(); + final String n = getName(); + if (p != null) { + p.appendFullName(r); + if (r.length() > 0) { + r.append('/'); + } + } + if (n != null) { + r.append(n); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeIterator.java new file mode 100644 index 0000000000..937baf6cc5 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeIterator.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.util.Iterator; + +/** + * A tree iterator iterates over a tree and all its members recursing into + * subtrees according to order. + * + * Default is to only visit leafs. An {@link Order} value can be supplied to + * make the iteration include Tree nodes as well either before or after the + * child nodes have been visited. + */ +public class TreeIterator implements Iterator<TreeEntry> { + + private Tree tree; + + private int index; + + private TreeIterator sub; + + private Order order; + + private boolean visitTreeNodes; + + private boolean hasVisitedTree; + + /** + * Traversal order + */ + public enum Order { + /** + * Visit node first, then leaves + */ + PREORDER, + + /** + * Visit leaves first, then node + */ + POSTORDER + } + + /** + * Construct a {@link TreeIterator} for visiting all non-tree nodes. + * + * @param start + */ + public TreeIterator(Tree start) { + this(start, Order.PREORDER, false); + } + + /** + * Construct a {@link TreeIterator} visiting all nodes in a tree in a given + * order. + * + * @param start Root node + * @param order {@link Order} + */ + public TreeIterator(Tree start, Order order) { + this(start, order, true); + } + + /** + * Construct a {@link TreeIterator} + * + * @param start First node to visit + * @param order Visitation {@link Order} + * @param visitTreeNode True to include tree node + */ + private TreeIterator(Tree start, Order order, boolean visitTreeNode) { + this.tree = start; + this.visitTreeNodes = visitTreeNode; + this.index = -1; + this.order = order; + if (!visitTreeNodes) + this.hasVisitedTree = true; + try { + step(); + } catch (IOException e) { + throw new Error(e); + } + } + + public TreeEntry next() { + try { + TreeEntry ret = nextTreeEntry(); + step(); + return ret; + } catch (IOException e) { + throw new Error(e); + } + } + + private TreeEntry nextTreeEntry() throws IOException { + TreeEntry ret; + if (sub != null) + ret = sub.nextTreeEntry(); + else { + if (index < 0 && order == Order.PREORDER) { + return tree; + } + if (order == Order.POSTORDER && index == tree.memberCount()) { + return tree; + } + ret = tree.members()[index]; + } + return ret; + } + + public boolean hasNext() { + try { + return hasNextTreeEntry(); + } catch (IOException e) { + throw new Error(e); + } + } + + private boolean hasNextTreeEntry() throws IOException { + if (tree == null) + return false; + return sub != null + || index < tree.memberCount() + || order == Order.POSTORDER && index == tree.memberCount(); + } + + private boolean step() throws IOException { + if (tree == null) + return false; + + if (sub != null) { + if (sub.step()) + return true; + sub = null; + } + + if (index < 0 && !hasVisitedTree && order == Order.PREORDER) { + hasVisitedTree = true; + return true; + } + + while (++index < tree.memberCount()) { + TreeEntry e = tree.members()[index]; + if (e instanceof Tree) { + sub = new TreeIterator((Tree) e, order, visitTreeNodes); + if (sub.hasNextTreeEntry()) + return true; + sub = null; + continue; + } + return true; + } + + if (index == tree.memberCount() && !hasVisitedTree + && order == Order.POSTORDER) { + hasVisitedTree = true; + return true; + } + return false; + } + + public void remove() { + throw new IllegalStateException( + "TreeIterator does not support remove()"); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeVisitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeVisitor.java new file mode 100644 index 0000000000..1745515460 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeVisitor.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2009, Jonas Fonseca <fonseca@diku.dk> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; + +/** + * A TreeVisitor is invoked depth first for every node in a tree and is expected + * to perform different actions. + */ +public interface TreeVisitor { + /** + * Visit to a tree node before child nodes are visited. + * + * @param t + * Tree + * @throws IOException + */ + public void startVisitTree(final Tree t) throws IOException; + + /** + * Visit to a tree node. after child nodes have been visited. + * + * @param t Tree + * @throws IOException + */ + public void endVisitTree(final Tree t) throws IOException; + + /** + * Visit to a blob. + * + * @param f Blob + * @throws IOException + */ + public void visitFile(final FileTreeEntry f) throws IOException; + + /** + * Visit to a symlink. + * + * @param s Symlink entry + * @throws IOException + */ + public void visitSymlink(final SymlinkTreeEntry s) throws IOException; + + /** + * Visit to a gitlink. + * + * @param s Gitlink entry + * @throws IOException + */ + public void visitGitlink(final GitlinkTreeEntry s) throws IOException; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeVisitorWithCurrentDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeVisitorWithCurrentDirectory.java new file mode 100644 index 0000000000..680bab6bec --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeVisitorWithCurrentDirectory.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2007-2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * Copyright (C) 2009, Vasyl' Vavrychuk <vvavrychuk@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +/** + * Abstract TreeVisitor for visiting all files known by a Tree. + */ +public abstract class TreeVisitorWithCurrentDirectory implements TreeVisitor { + private final ArrayList<File> stack = new ArrayList<File>(16); + + private File currentDirectory; + + TreeVisitorWithCurrentDirectory(final File rootDirectory) { + currentDirectory = rootDirectory; + } + + File getCurrentDirectory() { + return currentDirectory; + } + + public void startVisitTree(final Tree t) throws IOException { + stack.add(currentDirectory); + if (!t.isRoot()) { + currentDirectory = new File(currentDirectory, t.getName()); + } + } + + public void endVisitTree(final Tree t) throws IOException { + currentDirectory = stack.remove(stack.size() - 1); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Treeish.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Treeish.java new file mode 100644 index 0000000000..7da14172e4 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Treeish.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; + +/** + * Tree-ish is an interface for tree-like Git objects. + */ +public interface Treeish { + /** + * @return the id of this tree + */ + public ObjectId getTreeId(); + + /** + * @return the tree of this tree-ish object + * @throws IOException + */ + public Tree getTree() throws IOException; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/UnpackedObjectCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/UnpackedObjectCache.java new file mode 100644 index 0000000000..3cef48242d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/UnpackedObjectCache.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.lang.ref.SoftReference; + +class UnpackedObjectCache { + private static final int CACHE_SZ = 1024; + + private static final SoftReference<Entry> DEAD; + + private static int hash(final long position) { + return (((int) position) << 22) >>> 22; + } + + private static int maxByteCount; + + private static final Slot[] cache; + + private static Slot lruHead; + + private static Slot lruTail; + + private static int openByteCount; + + static { + DEAD = new SoftReference<Entry>(null); + maxByteCount = new WindowCacheConfig().getDeltaBaseCacheLimit(); + + cache = new Slot[CACHE_SZ]; + for (int i = 0; i < CACHE_SZ; i++) + cache[i] = new Slot(); + } + + static synchronized void reconfigure(final WindowCacheConfig cfg) { + final int dbLimit = cfg.getDeltaBaseCacheLimit(); + if (maxByteCount != dbLimit) { + maxByteCount = dbLimit; + releaseMemory(); + } + } + + static synchronized Entry get(final PackFile pack, final long position) { + final Slot e = cache[hash(position)]; + if (e.provider == pack && e.position == position) { + final Entry buf = e.data.get(); + if (buf != null) { + moveToHead(e); + return buf; + } + } + return null; + } + + static synchronized void store(final PackFile pack, final long position, + final byte[] data, final int objectType) { + if (data.length > maxByteCount) + return; // Too large to cache. + + final Slot e = cache[hash(position)]; + clearEntry(e); + + openByteCount += data.length; + releaseMemory(); + + e.provider = pack; + e.position = position; + e.sz = data.length; + e.data = new SoftReference<Entry>(new Entry(data, objectType)); + moveToHead(e); + } + + private static void releaseMemory() { + while (openByteCount > maxByteCount && lruTail != null) { + final Slot currOldest = lruTail; + final Slot nextOldest = currOldest.lruPrev; + + clearEntry(currOldest); + currOldest.lruPrev = null; + currOldest.lruNext = null; + + if (nextOldest == null) + lruHead = null; + else + nextOldest.lruNext = null; + lruTail = nextOldest; + } + } + + static synchronized void purge(final PackFile file) { + for (final Slot e : cache) { + if (e.provider == file) { + clearEntry(e); + unlink(e); + } + } + } + + private static void moveToHead(final Slot e) { + unlink(e); + e.lruPrev = null; + e.lruNext = lruHead; + if (lruHead != null) + lruHead.lruPrev = e; + else + lruTail = e; + lruHead = e; + } + + private static void unlink(final Slot e) { + final Slot prev = e.lruPrev; + final Slot next = e.lruNext; + if (prev != null) + prev.lruNext = next; + if (next != null) + next.lruPrev = prev; + } + + private static void clearEntry(final Slot e) { + openByteCount -= e.sz; + e.provider = null; + e.data = DEAD; + e.sz = 0; + } + + private UnpackedObjectCache() { + throw new UnsupportedOperationException(); + } + + static class Entry { + final byte[] data; + + final int type; + + Entry(final byte[] aData, final int aType) { + data = aData; + type = aType; + } + } + + private static class Slot { + Slot lruPrev; + + Slot lruNext; + + PackFile provider; + + long position; + + int sz; + + SoftReference<Entry> data = DEAD; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/UnpackedObjectLoader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/UnpackedObjectLoader.java new file mode 100644 index 0000000000..c31dfdee42 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/UnpackedObjectLoader.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.util.MutableInteger; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * Loose object loader. This class loads an object not stored in a pack. + */ +public class UnpackedObjectLoader extends ObjectLoader { + private final int objectType; + + private final int objectSize; + + private final byte[] bytes; + + /** + * Construct an ObjectLoader to read from the file. + * + * @param path + * location of the loose object to read. + * @param id + * expected identity of the object being loaded, if known. + * @throws FileNotFoundException + * the loose object file does not exist. + * @throws IOException + * the loose object file exists, but is corrupt. + */ + public UnpackedObjectLoader(final File path, final AnyObjectId id) + throws IOException { + this(NB.readFully(path), id); + } + + /** + * Construct an ObjectLoader from a loose object's compressed form. + * + * @param compressed + * entire content of the loose object file. + * @throws CorruptObjectException + * The compressed data supplied does not match the format for a + * valid loose object. + */ + public UnpackedObjectLoader(final byte[] compressed) + throws CorruptObjectException { + this(compressed, null); + } + + private UnpackedObjectLoader(final byte[] compressed, final AnyObjectId id) + throws CorruptObjectException { + // Try to determine if this is a legacy format loose object or + // a new style loose object. The legacy format was completely + // compressed with zlib so the first byte must be 0x78 (15-bit + // window size, deflated) and the first 16 bit word must be + // evenly divisible by 31. Otherwise its a new style loose + // object. + // + final Inflater inflater = InflaterCache.get(); + try { + final int fb = compressed[0] & 0xff; + if (fb == 0x78 && (((fb << 8) | compressed[1] & 0xff) % 31) == 0) { + inflater.setInput(compressed); + final byte[] hdr = new byte[64]; + int avail = 0; + while (!inflater.finished() && avail < hdr.length) + try { + avail += inflater.inflate(hdr, avail, hdr.length + - avail); + } catch (DataFormatException dfe) { + final CorruptObjectException coe; + coe = new CorruptObjectException(id, "bad stream"); + coe.initCause(dfe); + inflater.end(); + throw coe; + } + if (avail < 5) + throw new CorruptObjectException(id, "no header"); + + final MutableInteger p = new MutableInteger(); + objectType = Constants.decodeTypeString(id, hdr, (byte) ' ', p); + objectSize = RawParseUtils.parseBase10(hdr, p.value, p); + if (objectSize < 0) + throw new CorruptObjectException(id, "negative size"); + if (hdr[p.value++] != 0) + throw new CorruptObjectException(id, "garbage after size"); + bytes = new byte[objectSize]; + if (p.value < avail) + System.arraycopy(hdr, p.value, bytes, 0, avail - p.value); + decompress(id, inflater, avail - p.value); + } else { + int p = 0; + int c = compressed[p++] & 0xff; + final int typeCode = (c >> 4) & 7; + int size = c & 15; + int shift = 4; + while ((c & 0x80) != 0) { + c = compressed[p++] & 0xff; + size += (c & 0x7f) << shift; + shift += 7; + } + + switch (typeCode) { + case Constants.OBJ_COMMIT: + case Constants.OBJ_TREE: + case Constants.OBJ_BLOB: + case Constants.OBJ_TAG: + objectType = typeCode; + break; + default: + throw new CorruptObjectException(id, "invalid type"); + } + + objectSize = size; + bytes = new byte[objectSize]; + inflater.setInput(compressed, p, compressed.length - p); + decompress(id, inflater, 0); + } + } finally { + InflaterCache.release(inflater); + } + } + + private void decompress(final AnyObjectId id, final Inflater inf, int p) + throws CorruptObjectException { + try { + while (!inf.finished()) + p += inf.inflate(bytes, p, objectSize - p); + } catch (DataFormatException dfe) { + final CorruptObjectException coe; + coe = new CorruptObjectException(id, "bad stream"); + coe.initCause(dfe); + throw coe; + } + if (p != objectSize) + throw new CorruptObjectException(id, "incorrect length"); + } + + @Override + public int getType() { + return objectType; + } + + @Override + public long getSize() { + return objectSize; + } + + @Override + public byte[] getCachedBytes() { + return bytes; + } + + @Override + public int getRawType() { + return objectType; + } + + @Override + public long getRawSize() { + return objectSize; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/UserConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/UserConfig.java new file mode 100644 index 0000000000..28b3cd0277 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/UserConfig.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2009, Yann Simon <yann.simon.fr@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import org.eclipse.jgit.lib.Config.SectionParser; +import org.eclipse.jgit.util.SystemReader; + +/** The standard "user" configuration parameters. */ +public class UserConfig { + /** Key for {@link Config#get(SectionParser)}. */ + public static final Config.SectionParser<UserConfig> KEY = new SectionParser<UserConfig>() { + public UserConfig parse(final Config cfg) { + return new UserConfig(cfg); + } + }; + + private final String authorName; + + private final String authorEmail; + + private final String committerName; + + private final String committerEmail; + + private UserConfig(final Config rc) { + authorName = getNameInternal(rc, Constants.GIT_AUTHOR_NAME_KEY); + authorEmail = getEmailInternal(rc, Constants.GIT_AUTHOR_EMAIL_KEY); + + committerName = getNameInternal(rc, Constants.GIT_COMMITTER_NAME_KEY); + committerEmail = getEmailInternal(rc, Constants.GIT_COMMITTER_EMAIL_KEY); + } + + /** + * @return the author name as defined in the git variables and + * configurations. If no name could be found, try to use the system + * user name instead. + */ + public String getAuthorName() { + return authorName; + } + + /** + * @return the committer name as defined in the git variables and + * configurations. If no name could be found, try to use the system + * user name instead. + */ + public String getCommitterName() { + return committerName; + } + + /** + * @return the author email as defined in git variables and + * configurations. If no email could be found, try to + * propose one default with the user name and the + * host name. + */ + public String getAuthorEmail() { + return authorEmail; + } + + /** + * @return the committer email as defined in git variables and + * configurations. If no email could be found, try to + * propose one default with the user name and the + * host name. + */ + public String getCommitterEmail() { + return committerEmail; + } + + private static String getNameInternal(Config rc, String envKey) { + // try to get the user name from the local and global configurations. + String username = rc.getString("user", null, "name"); + + if (username == null) { + // try to get the user name for the system property GIT_XXX_NAME + username = system().getenv(envKey); + } + if (username == null) { + // get the system user name + username = system().getProperty(Constants.OS_USER_NAME_KEY); + } + if (username == null) { + username = Constants.UNKNOWN_USER_DEFAULT; + } + return username; + } + + private static String getEmailInternal(Config rc, String envKey) { + // try to get the email from the local and global configurations. + String email = rc.getString("user", null, "email"); + + if (email == null) { + // try to get the email for the system property GIT_XXX_EMAIL + email = system().getenv(envKey); + } + + if (email == null) { + // try to construct an email + String username = system().getProperty(Constants.OS_USER_NAME_KEY); + if (username == null){ + username = Constants.UNKNOWN_USER_DEFAULT; + } + email = username + "@" + system().getHostname(); + } + + return email; + } + + private static SystemReader system() { + return SystemReader.getInstance(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/WholePackedObjectLoader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WholePackedObjectLoader.java new file mode 100644 index 0000000000..31439d4891 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WholePackedObjectLoader.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.util.zip.DataFormatException; + +import org.eclipse.jgit.errors.CorruptObjectException; + +/** Reader for a non-delta (just deflated) object in a pack file. */ +class WholePackedObjectLoader extends PackedObjectLoader { + private static final int OBJ_COMMIT = Constants.OBJ_COMMIT; + + WholePackedObjectLoader(final PackFile pr, final long dataOffset, + final long objectOffset, final int type, final int size) { + super(pr, dataOffset, objectOffset); + objectType = type; + objectSize = size; + } + + @Override + public void materialize(final WindowCursor curs) throws IOException { + if (cachedBytes != null) { + return; + } + + if (objectType != OBJ_COMMIT) { + final UnpackedObjectCache.Entry cache = pack.readCache(dataOffset); + if (cache != null) { + curs.release(); + cachedBytes = cache.data; + return; + } + } + + try { + cachedBytes = pack.decompress(dataOffset, objectSize, curs); + curs.release(); + if (objectType != OBJ_COMMIT) + pack.saveCache(dataOffset, cachedBytes, objectType); + } catch (DataFormatException dfe) { + final CorruptObjectException coe; + coe = new CorruptObjectException("Object at " + dataOffset + " in " + + pack.getPackFile() + " has bad zlib stream"); + coe.initCause(dfe); + throw coe; + } + } + + @Override + public int getRawType() { + return objectType; + } + + @Override + public long getRawSize() { + return objectSize; + } + + @Override + public ObjectId getDeltaBase() { + return null; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/WindowCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WindowCache.java new file mode 100644 index 0000000000..9c21342637 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WindowCache.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.lang.ref.ReferenceQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Caches slices of a {@link PackFile} in memory for faster read access. + * <p> + * The WindowCache serves as a Java based "buffer cache", loading segments of a + * PackFile into the JVM heap prior to use. As JGit often wants to do reads of + * only tiny slices of a file, the WindowCache tries to smooth out these tiny + * reads into larger block-sized IO operations. + */ +public class WindowCache extends OffsetCache<ByteWindow, WindowCache.WindowRef> { + private static final int bits(int newSize) { + if (newSize < 4096) + throw new IllegalArgumentException("Invalid window size"); + if (Integer.bitCount(newSize) != 1) + throw new IllegalArgumentException("Window size must be power of 2"); + return Integer.numberOfTrailingZeros(newSize); + } + + private static volatile WindowCache cache; + + static { + reconfigure(new WindowCacheConfig()); + } + + /** + * Modify the configuration of the window cache. + * <p> + * The new configuration is applied immediately. If the new limits are + * smaller than what what is currently cached, older entries will be purged + * as soon as possible to allow the cache to meet the new limit. + * + * @param packedGitLimit + * maximum number of bytes to hold within this instance. + * @param packedGitWindowSize + * number of bytes per window within the cache. + * @param packedGitMMAP + * true to enable use of mmap when creating windows. + * @param deltaBaseCacheLimit + * number of bytes to hold in the delta base cache. + * @deprecated Use {@link WindowCacheConfig} instead. + */ + public static void reconfigure(final int packedGitLimit, + final int packedGitWindowSize, final boolean packedGitMMAP, + final int deltaBaseCacheLimit) { + final WindowCacheConfig c = new WindowCacheConfig(); + c.setPackedGitLimit(packedGitLimit); + c.setPackedGitWindowSize(packedGitWindowSize); + c.setPackedGitMMAP(packedGitMMAP); + c.setDeltaBaseCacheLimit(deltaBaseCacheLimit); + reconfigure(c); + } + + /** + * Modify the configuration of the window cache. + * <p> + * The new configuration is applied immediately. If the new limits are + * smaller than what what is currently cached, older entries will be purged + * as soon as possible to allow the cache to meet the new limit. + * + * @param cfg + * the new window cache configuration. + * @throws IllegalArgumentException + * the cache configuration contains one or more invalid + * settings, usually too low of a limit. + */ + public static void reconfigure(final WindowCacheConfig cfg) { + final WindowCache nc = new WindowCache(cfg); + final WindowCache oc = cache; + if (oc != null) + oc.removeAll(); + cache = nc; + UnpackedObjectCache.reconfigure(cfg); + } + + static WindowCache getInstance() { + return cache; + } + + static final ByteWindow get(final PackFile pack, final long offset) + throws IOException { + final WindowCache c = cache; + final ByteWindow r = c.getOrLoad(pack, c.toStart(offset)); + if (c != cache) { + // The cache was reconfigured while we were using the old one + // to load this window. The window is still valid, but our + // cache may think its still live. Ensure the window is removed + // from the old cache so resources can be released. + // + c.removeAll(); + } + return r; + } + + static final void purge(final PackFile pack) { + cache.removeAll(pack); + } + + private final int maxFiles; + + private final long maxBytes; + + private final boolean mmap; + + private final int windowSizeShift; + + private final int windowSize; + + private final AtomicInteger openFiles; + + private final AtomicLong openBytes; + + private WindowCache(final WindowCacheConfig cfg) { + super(tableSize(cfg), lockCount(cfg)); + maxFiles = cfg.getPackedGitOpenFiles(); + maxBytes = cfg.getPackedGitLimit(); + mmap = cfg.isPackedGitMMAP(); + windowSizeShift = bits(cfg.getPackedGitWindowSize()); + windowSize = 1 << windowSizeShift; + + openFiles = new AtomicInteger(); + openBytes = new AtomicLong(); + + if (maxFiles < 1) + throw new IllegalArgumentException("Open files must be >= 1"); + if (maxBytes < windowSize) + throw new IllegalArgumentException("Window size must be < limit"); + } + + int getOpenFiles() { + return openFiles.get(); + } + + long getOpenBytes() { + return openBytes.get(); + } + + @Override + protected int hash(final int packHash, final long off) { + return packHash + (int) (off >>> windowSizeShift); + } + + @Override + protected ByteWindow load(final PackFile pack, final long offset) + throws IOException { + if (pack.beginWindowCache()) + openFiles.incrementAndGet(); + try { + if (mmap) + return pack.mmap(offset, windowSize); + return pack.read(offset, windowSize); + } catch (IOException e) { + close(pack); + throw e; + } catch (RuntimeException e) { + close(pack); + throw e; + } catch (Error e) { + close(pack); + throw e; + } + } + + @Override + protected WindowRef createRef(final PackFile p, final long o, + final ByteWindow v) { + final WindowRef ref = new WindowRef(p, o, v, queue); + openBytes.addAndGet(ref.size); + return ref; + } + + @Override + protected void clear(final WindowRef ref) { + openBytes.addAndGet(-ref.size); + close(ref.pack); + } + + private void close(final PackFile pack) { + if (pack.endWindowCache()) + openFiles.decrementAndGet(); + } + + @Override + protected boolean isFull() { + return maxFiles < openFiles.get() || maxBytes < openBytes.get(); + } + + private long toStart(final long offset) { + return (offset >>> windowSizeShift) << windowSizeShift; + } + + private static int tableSize(final WindowCacheConfig cfg) { + final int wsz = cfg.getPackedGitWindowSize(); + final long limit = cfg.getPackedGitLimit(); + if (wsz <= 0) + throw new IllegalArgumentException("Invalid window size"); + if (limit < wsz) + throw new IllegalArgumentException("Window size must be < limit"); + return (int) Math.min(5 * (limit / wsz) / 2, 2000000000); + } + + private static int lockCount(final WindowCacheConfig cfg) { + return Math.max(cfg.getPackedGitOpenFiles(), 32); + } + + static class WindowRef extends OffsetCache.Ref<ByteWindow> { + final int size; + + WindowRef(final PackFile pack, final long position, final ByteWindow v, + final ReferenceQueue<ByteWindow> queue) { + super(pack, position, v, queue); + size = v.size(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/WindowCacheConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WindowCacheConfig.java new file mode 100644 index 0000000000..2d8aef34ca --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WindowCacheConfig.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +/** Configuration parameters for {@link WindowCache}. */ +public class WindowCacheConfig { + /** 1024 (number of bytes in one kibibyte/kilobyte) */ + public static final int KB = 1024; + + /** 1024 {@link #KB} (number of bytes in one mebibyte/megabyte) */ + public static final int MB = 1024 * KB; + + private int packedGitOpenFiles; + + private long packedGitLimit; + + private int packedGitWindowSize; + + private boolean packedGitMMAP; + + private int deltaBaseCacheLimit; + + /** Create a default configuration. */ + public WindowCacheConfig() { + packedGitOpenFiles = 128; + packedGitLimit = 10 * MB; + packedGitWindowSize = 8 * KB; + packedGitMMAP = false; + deltaBaseCacheLimit = 10 * MB; + } + + /** + * @return maximum number of streams to open at a time. Open packs count + * against the process limits. <b>Default is 128.</b> + */ + public int getPackedGitOpenFiles() { + return packedGitOpenFiles; + } + + /** + * @param fdLimit + * maximum number of streams to open at a time. Open packs count + * against the process limits + */ + public void setPackedGitOpenFiles(final int fdLimit) { + packedGitOpenFiles = fdLimit; + } + + /** + * @return maximum number bytes of heap memory to dedicate to caching pack + * file data. <b>Default is 10 MB.</b> + */ + public long getPackedGitLimit() { + return packedGitLimit; + } + + /** + * @param newLimit + * maximum number bytes of heap memory to dedicate to caching + * pack file data. + */ + public void setPackedGitLimit(final long newLimit) { + packedGitLimit = newLimit; + } + + /** + * @return size in bytes of a single window mapped or read in from the pack + * file. <b>Default is 8 KB.</b> + */ + public int getPackedGitWindowSize() { + return packedGitWindowSize; + } + + /** + * @param newSize + * size in bytes of a single window read in from the pack file. + */ + public void setPackedGitWindowSize(final int newSize) { + packedGitWindowSize = newSize; + } + + /** + * @return true enables use of Java NIO virtual memory mapping for windows; + * false reads entire window into a byte[] with standard read calls. + * <b>Default false.</b> + */ + public boolean isPackedGitMMAP() { + return packedGitMMAP; + } + + /** + * @param usemmap + * true enables use of Java NIO virtual memory mapping for + * windows; false reads entire window into a byte[] with standard + * read calls. + */ + public void setPackedGitMMAP(final boolean usemmap) { + packedGitMMAP = usemmap; + } + + /** + * @return maximum number of bytes to cache in {@link UnpackedObjectCache} + * for inflated, recently accessed objects, without delta chains. + * <b>Default 10 MB.</b> + */ + public int getDeltaBaseCacheLimit() { + return deltaBaseCacheLimit; + } + + /** + * @param newLimit + * maximum number of bytes to cache in + * {@link UnpackedObjectCache} for inflated, recently accessed + * objects, without delta chains. + */ + public void setDeltaBaseCacheLimit(final int newLimit) { + deltaBaseCacheLimit = newLimit; + } + + /** + * Update properties by setting fields from the configuration. + * <p> + * If a property is not defined in the configuration, then it is left + * unmodified. + * + * @param rc configuration to read properties from. + */ + public void fromConfig(final Config rc) { + setPackedGitOpenFiles(rc.getInt("core", null, "packedgitopenfiles", getPackedGitOpenFiles())); + setPackedGitLimit(rc.getLong("core", null, "packedgitlimit", getPackedGitLimit())); + setPackedGitWindowSize(rc.getInt("core", null, "packedgitwindowsize", getPackedGitWindowSize())); + setPackedGitMMAP(rc.getBoolean("core", null, "packedgitmmap", isPackedGitMMAP())); + setDeltaBaseCacheLimit(rc.getInt("core", null, "deltabasecachelimit", getDeltaBaseCacheLimit())); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/WindowCursor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WindowCursor.java new file mode 100644 index 0000000000..fcf43adf62 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WindowCursor.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.IOException; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** Active handle to a ByteWindow. */ +public final class WindowCursor { + /** Temporary buffer large enough for at least one raw object id. */ + final byte[] tempId = new byte[Constants.OBJECT_ID_LENGTH]; + + private Inflater inf; + + private ByteWindow window; + + /** + * Copy bytes from the window to a caller supplied buffer. + * + * @param pack + * the file the desired window is stored within. + * @param position + * position within the file to read from. + * @param dstbuf + * destination buffer to copy into. + * @param dstoff + * offset within <code>dstbuf</code> to start copying into. + * @param cnt + * number of bytes to copy. This value may exceed the number of + * bytes remaining in the window starting at offset + * <code>pos</code>. + * @return number of bytes actually copied; this may be less than + * <code>cnt</code> if <code>cnt</code> exceeded the number of + * bytes available. + * @throws IOException + * this cursor does not match the provider or id and the proper + * window could not be acquired through the provider's cache. + */ + int copy(final PackFile pack, long position, final byte[] dstbuf, + int dstoff, final int cnt) throws IOException { + final long length = pack.length; + int need = cnt; + while (need > 0 && position < length) { + pin(pack, position); + final int r = window.copy(position, dstbuf, dstoff, need); + position += r; + dstoff += r; + need -= r; + } + return cnt - need; + } + + /** + * Pump bytes into the supplied inflater as input. + * + * @param pack + * the file the desired window is stored within. + * @param position + * position within the file to read from. + * @param dstbuf + * destination buffer the inflater should output decompressed + * data to. + * @param dstoff + * current offset within <code>dstbuf</code> to inflate into. + * @return updated <code>dstoff</code> based on the number of bytes + * successfully inflated into <code>dstbuf</code>. + * @throws IOException + * this cursor does not match the provider or id and the proper + * window could not be acquired through the provider's cache. + * @throws DataFormatException + * the inflater encountered an invalid chunk of data. Data + * stream corruption is likely. + */ + int inflate(final PackFile pack, long position, final byte[] dstbuf, + int dstoff) throws IOException, DataFormatException { + if (inf == null) + inf = InflaterCache.get(); + else + inf.reset(); + for (;;) { + pin(pack, position); + dstoff = window.inflate(position, dstbuf, dstoff, inf); + if (inf.finished()) + return dstoff; + position = window.end; + } + } + + void inflateVerify(final PackFile pack, long position) + throws IOException, DataFormatException { + if (inf == null) + inf = InflaterCache.get(); + else + inf.reset(); + for (;;) { + pin(pack, position); + window.inflateVerify(position, inf); + if (inf.finished()) + return; + position = window.end; + } + } + + private void pin(final PackFile pack, final long position) + throws IOException { + final ByteWindow w = window; + if (w == null || !w.contains(pack, position)) { + // If memory is low, we may need what is in our window field to + // be cleaned up by the GC during the get for the next window. + // So we always clear it, even though we are just going to set + // it again. + // + window = null; + window = WindowCache.get(pack, position); + } + } + + /** Release the current window cursor. */ + public void release() { + window = null; + try { + InflaterCache.release(inf); + } finally { + inf = null; + } + } + + /** + * @param curs cursor to release; may be null. + * @return always null. + */ + public static WindowCursor release(final WindowCursor curs) { + if (curs != null) + curs.release(); + return null; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/WorkDirCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WorkDirCheckout.java new file mode 100644 index 0000000000..75cc3bdc5c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WorkDirCheckout.java @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Roger C. Soares <rogersoares@intelinet.com.br> + * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +import org.eclipse.jgit.errors.CheckoutConflictException; +import org.eclipse.jgit.lib.GitIndex.Entry; + +/** + * This class handles checking out one or two trees merging + * with the index (actually a tree too). + * + * Three-way merges are no performed. See {@link #setFailOnConflict(boolean)}. + */ +public class WorkDirCheckout { + Repository repo; + + File root; + + GitIndex index; + + private boolean failOnConflict = true; + + Tree merge; + + + /** + * If <code>true</code>, will scan first to see if it's possible to check out, + * otherwise throw {@link CheckoutConflictException}. If <code>false</code>, + * it will silently deal with the problem. + * @param failOnConflict + */ + public void setFailOnConflict(boolean failOnConflict) { + this.failOnConflict = failOnConflict; + } + + WorkDirCheckout(Repository repo, File workDir, + GitIndex oldIndex, GitIndex newIndex) throws IOException { + this.repo = repo; + this.root = workDir; + this.index = oldIndex; + this.merge = repo.mapTree(newIndex.writeTree()); + } + + /** + * Create a checkout class for checking out one tree, merging with the index + * + * @param repo + * @param root workdir + * @param index current index + * @param merge tree to check out + */ + public WorkDirCheckout(Repository repo, File root, + GitIndex index, Tree merge) { + this.repo = repo; + this.root = root; + this.index = index; + this.merge = merge; + } + + /** + * Create a checkout class for merging and checking our two trees and the index. + * + * @param repo + * @param root workdir + * @param head + * @param index + * @param merge + */ + public WorkDirCheckout(Repository repo, File root, Tree head, GitIndex index, Tree merge) { + this(repo, root, index, merge); + this.head = head; + } + + /** + * Execute this checkout + * + * @throws IOException + */ + public void checkout() throws IOException { + if (head == null) + prescanOneTree(); + else prescanTwoTrees(); + if (!conflicts.isEmpty()) { + if (failOnConflict) { + String[] entries = conflicts.toArray(new String[0]); + throw new CheckoutConflictException(entries); + } + } + + cleanUpConflicts(); + if (head == null) + checkoutOutIndexNoHead(); + else checkoutTwoTrees(); + } + + private void checkoutTwoTrees() throws FileNotFoundException, IOException { + for (String path : removed) { + index.remove(root, new File(root, path)); + } + + for (java.util.Map.Entry<String, ObjectId> entry : updated.entrySet()) { + Entry newEntry = index.addEntry(merge.findBlobMember(entry.getKey())); + index.checkoutEntry(root, newEntry); + } + } + + ArrayList<String> conflicts = new ArrayList<String>(); + ArrayList<String> removed = new ArrayList<String>(); + + Tree head = null; + + HashMap<String, ObjectId> updated = new HashMap<String, ObjectId>(); + + private void checkoutOutIndexNoHead() throws IOException { + new IndexTreeWalker(index, merge, root, new AbstractIndexTreeVisitor() { + public void visitEntry(TreeEntry m, Entry i, File f) throws IOException { + if (m == null) { + index.remove(root, f); + return; + } + + boolean needsCheckout = false; + if (i == null) + needsCheckout = true; + else if (i.getObjectId().equals(m.getId())) { + if (i.isModified(root, true)) + needsCheckout = true; + } else needsCheckout = true; + + if (needsCheckout) { + Entry newEntry = index.addEntry(m); + index.checkoutEntry(root, newEntry); + } + } + }).walk(); + } + + private void cleanUpConflicts() throws CheckoutConflictException { + for (String c : conflicts) { + File conflict = new File(root, c); + if (!conflict.delete()) + throw new CheckoutConflictException("Cannot delete file: " + c); + removeEmptyParents(conflict); + } + for (String r : removed) { + File file = new File(root, r); + file.delete(); + removeEmptyParents(file); + } + } + + private void removeEmptyParents(File f) { + File parentFile = f.getParentFile(); + while (!parentFile.equals(root)) { + if (parentFile.list().length == 0) + parentFile.delete(); + else break; + + parentFile = parentFile.getParentFile(); + } + } + + void prescanOneTree() throws IOException { + new IndexTreeWalker(index, merge, root, new AbstractIndexTreeVisitor() { + public void visitEntry(TreeEntry m, Entry i, File file) throws IOException { + if (m != null) { + if (!file.isFile()) { + checkConflictsWithFile(file); + } + } else { + if (file.exists()) { + removed.add(i.getName()); + conflicts.remove(i.getName()); + } + } + } + }).walk(); + conflicts.removeAll(removed); + } + + private ArrayList<String> listFiles(File file) { + ArrayList<String> list = new ArrayList<String>(); + listFiles(file, list); + return list; + } + + private void listFiles(File dir, ArrayList<String> list) { + for (File f : dir.listFiles()) { + if (f.isDirectory()) + listFiles(f, list); + else { + list.add(Repository.stripWorkDir(root, f)); + } + } + } + + /** + * @return a list of conflicts created by this checkout + */ + public ArrayList<String> getConflicts() { + return conflicts; + } + + /** + * @return a list of all files removed by this checkout + */ + public ArrayList<String> getRemoved() { + return removed; + } + + void prescanTwoTrees() throws IOException { + new IndexTreeWalker(index, head, merge, root, new AbstractIndexTreeVisitor() { + public void visitEntry(TreeEntry treeEntry, TreeEntry auxEntry, + Entry indexEntry, File file) throws IOException { + if (treeEntry instanceof Tree || auxEntry instanceof Tree) { + throw new IllegalArgumentException("Can't pass me a tree!"); + } + processEntry(treeEntry, auxEntry, indexEntry); + } + + @Override + public void finishVisitTree(Tree tree, Tree auxTree, String curDir) throws IOException { + if (curDir.length() == 0) return; + + if (auxTree != null) { + if (index.getEntry(curDir) != null) + removed.add(curDir); + } + } + + }).walk(); + + // if there's a conflict, don't list it under + // to-be-removed, since that messed up our next + // section + removed.removeAll(conflicts); + + for (String path : updated.keySet()) { + if (index.getEntry(path) == null) { + File file = new File(root, path); + if (file.isFile()) + conflicts.add(path); + else if (file.isDirectory()) { + checkConflictsWithFile(file); + } + } + } + + + conflicts.removeAll(removed); + } + + void processEntry(TreeEntry h, TreeEntry m, Entry i) throws IOException { + ObjectId iId = (i == null ? null : i.getObjectId()); + ObjectId mId = (m == null ? null : m.getId()); + ObjectId hId = (h == null ? null : h.getId()); + + String name = (i != null ? i.getName() : + (h != null ? h.getFullName() : + m.getFullName())); + + if (i == null) { + /* + I (index) H M Result + ------------------------------------------------------- + 0 nothing nothing nothing (does not happen) + 1 nothing nothing exists use M + 2 nothing exists nothing remove path from index + 3 nothing exists exists use M */ + + if (h == null) { + updated.put(name,mId); + } else if (m == null) { + removed.add(name); + } else { + updated.put(name, mId); + } + } else if (h == null) { + /* + clean I==H I==M H M Result + ----------------------------------------------------- + 4 yes N/A N/A nothing nothing keep index + 5 no N/A N/A nothing nothing keep index + + 6 yes N/A yes nothing exists keep index + 7 no N/A yes nothing exists keep index + 8 yes N/A no nothing exists fail + 9 no N/A no nothing exists fail */ + + if (m == null || mId.equals(iId)) { + if (hasParentBlob(merge, name)) { + if (i.isModified(root, true)) { + conflicts.add(name); + } else { + removed.add(name); + } + } + } else { + conflicts.add(name); + } + } else if (m == null) { + /* + 10 yes yes N/A exists nothing remove path from index + 11 no yes N/A exists nothing fail + 12 yes no N/A exists nothing fail + 13 no no N/A exists nothing fail + */ + + if (hId.equals(iId)) { + if (i.isModified(root, true)) { + conflicts.add(name); + } else { + removed.add(name); + } + } else { + conflicts.add(name); + } + } else { + if (!hId.equals(mId) && !hId.equals(iId) + && !mId.equals(iId)) { + conflicts.add(name); + } else if (hId.equals(iId) && !mId.equals(iId)) { + if (i.isModified(root, true)) + conflicts.add(name); + else updated.put(name, mId); + } + } + } + + private boolean hasParentBlob(Tree t, String name) throws IOException { + if (name.indexOf("/") == -1) return false; + + String parent = name.substring(0, name.lastIndexOf("/")); + if (t.findBlobMember(parent) != null) + return true; + return hasParentBlob(t, parent); + } + + private void checkConflictsWithFile(File file) { + if (file.isDirectory()) { + ArrayList<String> childFiles = listFiles(file); + conflicts.addAll(childFiles); + } else { + File parent = file.getParentFile(); + while (!parent.equals(root)) { + if (parent.isDirectory()) + break; + if (parent.isFile()) { + conflicts.add(Repository.stripWorkDir(root, parent)); + break; + } + parent = parent.getParentFile(); + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/WriteTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WriteTree.java new file mode 100644 index 0000000000..5bb4e535e0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WriteTree.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2009, Jonas Fonseca <fonseca@diku.dk> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.lib; + +import java.io.File; +import java.io.IOException; + +import org.eclipse.jgit.errors.GitlinksNotSupportedException; +import org.eclipse.jgit.errors.SymlinksNotSupportedException; + +/** + * A tree visitor for writing a directory tree to the git object database. Blob + * data is fetched from the files, not the cached blobs. + */ +public class WriteTree extends TreeVisitorWithCurrentDirectory { + private final ObjectWriter ow; + + /** + * Construct a WriteTree for a given directory + * + * @param sourceDirectory + * @param db + */ + public WriteTree(final File sourceDirectory, final Repository db) { + super(sourceDirectory); + ow = new ObjectWriter(db); + } + + public void visitFile(final FileTreeEntry f) throws IOException { + f.setId(ow.writeBlob(new File(getCurrentDirectory(), f.getName()))); + } + + public void visitSymlink(final SymlinkTreeEntry s) throws IOException { + if (s.isModified()) { + throw new SymlinksNotSupportedException("Symlink \"" + + s.getFullName() + + "\" cannot be written as the link target" + + " cannot be read from within Java."); + } + } + + public void endVisitTree(final Tree t) throws IOException { + super.endVisitTree(t); + t.setId(ow.writeTree(t)); + } + + public void visitGitlink(GitlinkTreeEntry s) throws IOException { + if (s.isModified()) { + throw new GitlinksNotSupportedException(s.getFullName()); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeStrategy.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeStrategy.java new file mode 100644 index 0000000000..e7d84c68ac --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeStrategy.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2009, Matthias Sohn <matthias.sohn@sap.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.merge; + +import java.util.HashMap; + +import org.eclipse.jgit.lib.Repository; + +/** + * A method of combining two or more trees together to form an output tree. + * <p> + * Different strategies may employ different techniques for deciding which paths + * (and ObjectIds) to carry from the input trees into the final output tree. + */ +public abstract class MergeStrategy { + /** Simple strategy that sets the output tree to the first input tree. */ + public static final MergeStrategy OURS = new StrategyOneSided("ours", 0); + + /** Simple strategy that sets the output tree to the second input tree. */ + public static final MergeStrategy THEIRS = new StrategyOneSided("theirs", 1); + + /** Simple strategy to merge paths, without simultaneous edits. */ + public static final ThreeWayMergeStrategy SIMPLE_TWO_WAY_IN_CORE = new StrategySimpleTwoWayInCore(); + + private static final HashMap<String, MergeStrategy> STRATEGIES = new HashMap<String, MergeStrategy>(); + + static { + register(OURS); + register(THEIRS); + register(SIMPLE_TWO_WAY_IN_CORE); + } + + /** + * Register a merge strategy so it can later be obtained by name. + * + * @param imp + * the strategy to register. + * @throws IllegalArgumentException + * a strategy by the same name has already been registered. + */ + public static void register(final MergeStrategy imp) { + register(imp.getName(), imp); + } + + /** + * Register a merge strategy so it can later be obtained by name. + * + * @param name + * name the strategy can be looked up under. + * @param imp + * the strategy to register. + * @throws IllegalArgumentException + * a strategy by the same name has already been registered. + */ + public static synchronized void register(final String name, + final MergeStrategy imp) { + if (STRATEGIES.containsKey(name)) + throw new IllegalArgumentException("Merge strategy \"" + name + + "\" already exists as a default strategy"); + STRATEGIES.put(name, imp); + } + + /** + * Locate a strategy by name. + * + * @param name + * name of the strategy to locate. + * @return the strategy instance; null if no strategy matches the name. + */ + public static synchronized MergeStrategy get(final String name) { + return STRATEGIES.get(name); + } + + /** + * Get all registered strategies. + * + * @return the registered strategy instances. No inherit order is returned; + * the caller may modify (and/or sort) the returned array if + * necessary to obtain a reasonable ordering. + */ + public static synchronized MergeStrategy[] get() { + final MergeStrategy[] r = new MergeStrategy[STRATEGIES.size()]; + STRATEGIES.values().toArray(r); + return r; + } + + /** @return default name of this strategy implementation. */ + public abstract String getName(); + + /** + * Create a new merge instance. + * + * @param db + * repository database the merger will read from, and eventually + * write results back to. + * @return the new merge instance which implements this strategy. + */ + public abstract Merger newMerger(Repository db); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java new file mode 100644 index 0000000000..275a6d68ff --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.merge; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectWriter; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.WindowCursor; +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.revwalk.filter.RevFilter; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.EmptyTreeIterator; + +/** + * Instance of a specific {@link MergeStrategy} for a single {@link Repository}. + */ +public abstract class Merger { + /** The repository this merger operates on. */ + protected final Repository db; + + /** A RevWalk for computing merge bases, or listing incoming commits. */ + protected final RevWalk walk; + + private ObjectWriter writer; + + /** The original objects supplied in the merge; this can be any tree-ish. */ + protected RevObject[] sourceObjects; + + /** If {@link #sourceObjects}[i] is a commit, this is the commit. */ + protected RevCommit[] sourceCommits; + + /** The trees matching every entry in {@link #sourceObjects}. */ + protected RevTree[] sourceTrees; + + /** + * Create a new merge instance for a repository. + * + * @param local + * the repository this merger will read and write data on. + */ + protected Merger(final Repository local) { + db = local; + walk = new RevWalk(db); + } + + /** + * @return the repository this merger operates on. + */ + public Repository getRepository() { + return db; + } + + /** + * @return an object writer to create objects in {@link #getRepository()}. + */ + public ObjectWriter getObjectWriter() { + if (writer == null) + writer = new ObjectWriter(getRepository()); + return writer; + } + + /** + * Merge together two or more tree-ish objects. + * <p> + * Any tree-ish may be supplied as inputs. Commits and/or tags pointing at + * trees or commits may be passed as input objects. + * + * @param tips + * source trees to be combined together. The merge base is not + * included in this set. + * @return true if the merge was completed without conflicts; false if the + * merge strategy cannot handle this merge or there were conflicts + * preventing it from automatically resolving all paths. + * @throws IncorrectObjectTypeException + * one of the input objects is not a commit, but the strategy + * requires it to be a commit. + * @throws IOException + * one or more sources could not be read, or outputs could not + * be written to the Repository. + */ + public boolean merge(final AnyObjectId[] tips) throws IOException { + sourceObjects = new RevObject[tips.length]; + for (int i = 0; i < tips.length; i++) + sourceObjects[i] = walk.parseAny(tips[i]); + + sourceCommits = new RevCommit[sourceObjects.length]; + for (int i = 0; i < sourceObjects.length; i++) { + try { + sourceCommits[i] = walk.parseCommit(sourceObjects[i]); + } catch (IncorrectObjectTypeException err) { + sourceCommits[i] = null; + } + } + + sourceTrees = new RevTree[sourceObjects.length]; + for (int i = 0; i < sourceObjects.length; i++) + sourceTrees[i] = walk.parseTree(sourceObjects[i]); + + return mergeImpl(); + } + + /** + * Create an iterator to walk the merge base of two commits. + * + * @param aIdx + * index of the first commit in {@link #sourceObjects}. + * @param bIdx + * index of the second commit in {@link #sourceObjects}. + * @return the new iterator + * @throws IncorrectObjectTypeException + * one of the input objects is not a commit. + * @throws IOException + * objects are missing or multiple merge bases were found. + */ + protected AbstractTreeIterator mergeBase(final int aIdx, final int bIdx) + throws IOException { + if (sourceCommits[aIdx] == null) + throw new IncorrectObjectTypeException(sourceObjects[aIdx], + Constants.TYPE_COMMIT); + if (sourceCommits[bIdx] == null) + throw new IncorrectObjectTypeException(sourceObjects[bIdx], + Constants.TYPE_COMMIT); + + walk.reset(); + walk.setRevFilter(RevFilter.MERGE_BASE); + walk.markStart(sourceCommits[aIdx]); + walk.markStart(sourceCommits[bIdx]); + final RevCommit base = walk.next(); + if (base == null) + return new EmptyTreeIterator(); + final RevCommit base2 = walk.next(); + if (base2 != null) { + throw new IOException("Multiple merge bases for:" + "\n " + + sourceCommits[aIdx].name() + "\n " + + sourceCommits[bIdx].name() + "found:" + "\n " + + base.name() + "\n " + base2.name()); + } + return openTree(base.getTree()); + } + + /** + * Open an iterator over a tree. + * + * @param treeId + * the tree to scan; must be a tree (not a treeish). + * @return an iterator for the tree. + * @throws IncorrectObjectTypeException + * the input object is not a tree. + * @throws IOException + * the tree object is not found or cannot be read. + */ + protected AbstractTreeIterator openTree(final AnyObjectId treeId) + throws IncorrectObjectTypeException, IOException { + final WindowCursor curs = new WindowCursor(); + try { + return new CanonicalTreeParser(null, db, treeId, curs); + } finally { + curs.release(); + } + } + + /** + * Execute the merge. + * <p> + * This method is called from {@link #merge(AnyObjectId[])} after the + * {@link #sourceObjects}, {@link #sourceCommits} and {@link #sourceTrees} + * have been populated. + * + * @return true if the merge was completed without conflicts; false if the + * merge strategy cannot handle this merge or there were conflicts + * preventing it from automatically resolving all paths. + * @throws IncorrectObjectTypeException + * one of the input objects is not a commit, but the strategy + * requires it to be a commit. + * @throws IOException + * one or more sources could not be read, or outputs could not + * be written to the Repository. + */ + protected abstract boolean mergeImpl() throws IOException; + + /** + * @return resulting tree, if {@link #merge(AnyObjectId[])} returned true. + */ + public abstract ObjectId getResultTreeId(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyOneSided.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyOneSided.java new file mode 100644 index 0000000000..c941af9482 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategyOneSided.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.merge; + +import java.io.IOException; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; + +/** + * Trivial merge strategy to make the resulting tree exactly match an input. + * <p> + * This strategy can be used to cauterize an entire side branch of history, by + * setting the output tree to one of the inputs, and ignoring any of the paths + * of the other inputs. + */ +public class StrategyOneSided extends MergeStrategy { + private final String strategyName; + + private final int treeIndex; + + /** + * Create a new merge strategy to select a specific input tree. + * + * @param name + * name of this strategy. + * @param index + * the position of the input tree to accept as the result. + */ + protected StrategyOneSided(final String name, final int index) { + strategyName = name; + treeIndex = index; + } + + @Override + public String getName() { + return strategyName; + } + + @Override + public Merger newMerger(final Repository db) { + return new OneSide(db, treeIndex); + } + + static class OneSide extends Merger { + private final int treeIndex; + + protected OneSide(final Repository local, final int index) { + super(local); + treeIndex = index; + } + + @Override + protected boolean mergeImpl() throws IOException { + return treeIndex < sourceTrees.length; + } + + @Override + public ObjectId getResultTreeId() { + return sourceTrees[treeIndex]; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategySimpleTwoWayInCore.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategySimpleTwoWayInCore.java new file mode 100644 index 0000000000..6cd244599e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/StrategySimpleTwoWayInCore.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.merge; + +import java.io.IOException; + +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.UnmergedPathException; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; +import org.eclipse.jgit.treewalk.NameConflictTreeWalk; + +/** + * Merges two commits together in-memory, ignoring any working directory. + * <p> + * The strategy chooses a path from one of the two input trees if the path is + * unchanged in the other relative to their common merge base tree. This is a + * trivial 3-way merge (at the file path level only). + * <p> + * Modifications of the same file path (content and/or file mode) by both input + * trees will cause a merge conflict, as this strategy does not attempt to merge + * file contents. + */ +public class StrategySimpleTwoWayInCore extends ThreeWayMergeStrategy { + /** Create a new instance of the strategy. */ + protected StrategySimpleTwoWayInCore() { + // + } + + @Override + public String getName() { + return "simple-two-way-in-core"; + } + + @Override + public ThreeWayMerger newMerger(final Repository db) { + return new InCoreMerger(db); + } + + private static class InCoreMerger extends ThreeWayMerger { + private static final int T_BASE = 0; + + private static final int T_OURS = 1; + + private static final int T_THEIRS = 2; + + private final NameConflictTreeWalk tw; + + private final DirCache cache; + + private DirCacheBuilder builder; + + private ObjectId resultTree; + + InCoreMerger(final Repository local) { + super(local); + tw = new NameConflictTreeWalk(db); + cache = DirCache.newInCore(); + } + + @Override + protected boolean mergeImpl() throws IOException { + tw.reset(); + tw.addTree(mergeBase()); + tw.addTree(sourceTrees[0]); + tw.addTree(sourceTrees[1]); + + boolean hasConflict = false; + builder = cache.builder(); + while (tw.next()) { + final int modeO = tw.getRawMode(T_OURS); + final int modeT = tw.getRawMode(T_THEIRS); + if (modeO == modeT && tw.idEqual(T_OURS, T_THEIRS)) { + add(T_OURS, DirCacheEntry.STAGE_0); + continue; + } + + final int modeB = tw.getRawMode(T_BASE); + if (modeB == modeO && tw.idEqual(T_BASE, T_OURS)) + add(T_THEIRS, DirCacheEntry.STAGE_0); + else if (modeB == modeT && tw.idEqual(T_BASE, T_THEIRS)) + add(T_OURS, DirCacheEntry.STAGE_0); + else if (tw.isSubtree()) { + if (nonTree(modeB)) { + add(T_BASE, DirCacheEntry.STAGE_1); + hasConflict = true; + } + if (nonTree(modeO)) { + add(T_OURS, DirCacheEntry.STAGE_2); + hasConflict = true; + } + if (nonTree(modeT)) { + add(T_THEIRS, DirCacheEntry.STAGE_3); + hasConflict = true; + } + tw.enterSubtree(); + } else { + add(T_BASE, DirCacheEntry.STAGE_1); + add(T_OURS, DirCacheEntry.STAGE_2); + add(T_THEIRS, DirCacheEntry.STAGE_3); + hasConflict = true; + } + } + builder.finish(); + builder = null; + + if (hasConflict) + return false; + try { + resultTree = cache.writeTree(getObjectWriter()); + return true; + } catch (UnmergedPathException upe) { + resultTree = null; + return false; + } + } + + private static boolean nonTree(final int mode) { + return mode != 0 && !FileMode.TREE.equals(mode); + } + + private void add(final int tree, final int stage) throws IOException { + final AbstractTreeIterator i = getTree(tree); + if (i != null) { + if (FileMode.TREE.equals(tw.getRawMode(tree))) { + builder.addTree(tw.getRawPath(), stage, db, tw + .getObjectId(tree)); + } else { + final DirCacheEntry e; + + e = new DirCacheEntry(tw.getRawPath(), stage); + e.setObjectIdFromRaw(i.idBuffer(), i.idOffset()); + e.setFileMode(tw.getFileMode(tree)); + builder.add(e); + } + } + } + + private AbstractTreeIterator getTree(final int tree) { + return tw.getTree(tree, AbstractTreeIterator.class); + } + + @Override + public ObjectId getResultTreeId() { + return resultTree; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMergeStrategy.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMergeStrategy.java new file mode 100644 index 0000000000..343d8973e9 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMergeStrategy.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.merge; + +import org.eclipse.jgit.lib.Repository; + +/** A merge strategy to merge 2 trees, using a common base ancestor tree. */ +public abstract class ThreeWayMergeStrategy extends MergeStrategy { + @Override + public abstract ThreeWayMerger newMerger(Repository db); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java new file mode 100644 index 0000000000..bb23d0ee87 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.merge; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; + +/** A merge of 2 trees, using a common base ancestor tree. */ +public abstract class ThreeWayMerger extends Merger { + private RevTree baseTree; + + /** + * Create a new merge instance for a repository. + * + * @param local + * the repository this merger will read and write data on. + */ + protected ThreeWayMerger(final Repository local) { + super(local); + } + + /** + * Set the common ancestor tree. + * + * @param id + * common base treeish; null to automatically compute the common + * base from the input commits during + * {@link #merge(AnyObjectId, AnyObjectId)}. + * @throws IncorrectObjectTypeException + * the object is not a treeish. + * @throws MissingObjectException + * the object does not exist. + * @throws IOException + * the object could not be read. + */ + public void setBase(final AnyObjectId id) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + if (id != null) { + baseTree = walk.parseTree(id); + } else { + baseTree = null; + } + } + + /** + * Merge together two tree-ish objects. + * <p> + * Any tree-ish may be supplied as inputs. Commits and/or tags pointing at + * trees or commits may be passed as input objects. + * + * @param a + * source tree to be combined together. + * @param b + * source tree to be combined together. + * @return true if the merge was completed without conflicts; false if the + * merge strategy cannot handle this merge or there were conflicts + * preventing it from automatically resolving all paths. + * @throws IncorrectObjectTypeException + * one of the input objects is not a commit, but the strategy + * requires it to be a commit. + * @throws IOException + * one or more sources could not be read, or outputs could not + * be written to the Repository. + */ + public boolean merge(final AnyObjectId a, final AnyObjectId b) + throws IOException { + return merge(new AnyObjectId[] { a, b }); + } + + @Override + public boolean merge(final AnyObjectId[] tips) throws IOException { + if (tips.length != 2) + return false; + return super.merge(tips); + } + + /** + * Create an iterator to walk the merge base. + * + * @return an iterator over the caller-specified merge base, or the natural + * merge base of the two input commits. + * @throws IOException + */ + protected AbstractTreeIterator mergeBase() throws IOException { + if (baseTree != null) + return openTree(baseTree); + return mergeBase(0, 1); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/BinaryHunk.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/BinaryHunk.java new file mode 100644 index 0000000000..340b67456e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/BinaryHunk.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.patch; + +import static org.eclipse.jgit.lib.Constants.encodeASCII; +import static org.eclipse.jgit.util.RawParseUtils.match; +import static org.eclipse.jgit.util.RawParseUtils.nextLF; +import static org.eclipse.jgit.util.RawParseUtils.parseBase10; + +/** Part of a "GIT binary patch" to describe the pre-image or post-image */ +public class BinaryHunk { + private static final byte[] LITERAL = encodeASCII("literal "); + + private static final byte[] DELTA = encodeASCII("delta "); + + /** Type of information stored in a binary hunk. */ + public static enum Type { + /** The full content is stored, deflated. */ + LITERAL_DEFLATED, + + /** A Git pack-style delta is stored, deflated. */ + DELTA_DEFLATED; + } + + private final FileHeader file; + + /** Offset within {@link #file}.buf to the "literal" or "delta " line. */ + final int startOffset; + + /** Position 1 past the end of this hunk within {@link #file}'s buf. */ + int endOffset; + + /** Type of the data meaning. */ + private Type type; + + /** Inflated length of the data. */ + private int length; + + BinaryHunk(final FileHeader fh, final int offset) { + file = fh; + startOffset = offset; + } + + /** @return header for the file this hunk applies to */ + public FileHeader getFileHeader() { + return file; + } + + /** @return the byte array holding this hunk's patch script. */ + public byte[] getBuffer() { + return file.buf; + } + + /** @return offset the start of this hunk in {@link #getBuffer()}. */ + public int getStartOffset() { + return startOffset; + } + + /** @return offset one past the end of the hunk in {@link #getBuffer()}. */ + public int getEndOffset() { + return endOffset; + } + + /** @return type of this binary hunk */ + public Type getType() { + return type; + } + + /** @return inflated size of this hunk's data */ + public int getSize() { + return length; + } + + int parseHunk(int ptr, final int end) { + final byte[] buf = file.buf; + + if (match(buf, ptr, LITERAL) >= 0) { + type = Type.LITERAL_DEFLATED; + length = parseBase10(buf, ptr + LITERAL.length, null); + + } else if (match(buf, ptr, DELTA) >= 0) { + type = Type.DELTA_DEFLATED; + length = parseBase10(buf, ptr + DELTA.length, null); + + } else { + // Not a valid binary hunk. Signal to the caller that + // we cannot parse any further and that this line should + // be treated otherwise. + // + return -1; + } + ptr = nextLF(buf, ptr); + + // Skip until the first blank line; that is the end of the binary + // encoded information in this hunk. To save time we don't do a + // validation of the binary data at this point. + // + while (ptr < end) { + final boolean empty = buf[ptr] == '\n'; + ptr = nextLF(buf, ptr); + if (empty) + break; + } + + return ptr; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedFileHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedFileHeader.java new file mode 100644 index 0000000000..e95c026ddc --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedFileHeader.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.patch; + +import static org.eclipse.jgit.lib.Constants.encodeASCII; +import static org.eclipse.jgit.util.RawParseUtils.match; +import static org.eclipse.jgit.util.RawParseUtils.nextLF; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.FileMode; + +/** + * A file in the Git "diff --cc" or "diff --combined" format. + * <p> + * A combined diff shows an n-way comparison between two or more ancestors and + * the final revision. Its primary function is to perform code reviews on a + * merge which introduces changes not in any ancestor. + */ +public class CombinedFileHeader extends FileHeader { + private static final byte[] MODE = encodeASCII("mode "); + + private AbbreviatedObjectId[] oldIds; + + private FileMode[] oldModes; + + CombinedFileHeader(final byte[] b, final int offset) { + super(b, offset); + } + + @Override + @SuppressWarnings("unchecked") + public List<? extends CombinedHunkHeader> getHunks() { + return (List<CombinedHunkHeader>) super.getHunks(); + } + + /** @return number of ancestor revisions mentioned in this diff. */ + @Override + public int getParentCount() { + return oldIds.length; + } + + /** @return get the file mode of the first parent. */ + @Override + public FileMode getOldMode() { + return getOldMode(0); + } + + /** + * Get the file mode of the nth ancestor + * + * @param nthParent + * the ancestor to get the mode of + * @return the mode of the requested ancestor. + */ + public FileMode getOldMode(final int nthParent) { + return oldModes[nthParent]; + } + + /** @return get the object id of the first parent. */ + @Override + public AbbreviatedObjectId getOldId() { + return getOldId(0); + } + + /** + * Get the ObjectId of the nth ancestor + * + * @param nthParent + * the ancestor to get the object id of + * @return the id of the requested ancestor. + */ + public AbbreviatedObjectId getOldId(final int nthParent) { + return oldIds[nthParent]; + } + + @Override + public String getScriptText(final Charset ocs, final Charset ncs) { + final Charset[] cs = new Charset[getParentCount() + 1]; + Arrays.fill(cs, ocs); + cs[getParentCount()] = ncs; + return getScriptText(cs); + } + + /** + * Convert the patch script for this file into a string. + * + * @param charsetGuess + * optional array to suggest the character set to use when + * decoding each file's line. If supplied the array must have a + * length of <code>{@link #getParentCount()} + 1</code> + * representing the old revision character sets and the new + * revision character set. + * @return the patch script, as a Unicode string. + */ + @Override + public String getScriptText(final Charset[] charsetGuess) { + return super.getScriptText(charsetGuess); + } + + @Override + int parseGitHeaders(int ptr, final int end) { + while (ptr < end) { + final int eol = nextLF(buf, ptr); + if (isHunkHdr(buf, ptr, end) >= 1) { + // First hunk header; break out and parse them later. + break; + + } else if (match(buf, ptr, OLD_NAME) >= 0) { + parseOldName(ptr, eol); + + } else if (match(buf, ptr, NEW_NAME) >= 0) { + parseNewName(ptr, eol); + + } else if (match(buf, ptr, INDEX) >= 0) { + parseIndexLine(ptr + INDEX.length, eol); + + } else if (match(buf, ptr, MODE) >= 0) { + parseModeLine(ptr + MODE.length, eol); + + } else if (match(buf, ptr, NEW_FILE_MODE) >= 0) { + parseNewFileMode(ptr, eol); + + } else if (match(buf, ptr, DELETED_FILE_MODE) >= 0) { + parseDeletedFileMode(ptr + DELETED_FILE_MODE.length, eol); + + } else { + // Probably an empty patch (stat dirty). + break; + } + + ptr = eol; + } + return ptr; + } + + @Override + protected void parseIndexLine(int ptr, final int eol) { + // "index $asha1,$bsha1..$csha1" + // + final List<AbbreviatedObjectId> ids = new ArrayList<AbbreviatedObjectId>(); + while (ptr < eol) { + final int comma = nextLF(buf, ptr, ','); + if (eol <= comma) + break; + ids.add(AbbreviatedObjectId.fromString(buf, ptr, comma - 1)); + ptr = comma; + } + + oldIds = new AbbreviatedObjectId[ids.size() + 1]; + ids.toArray(oldIds); + final int dot2 = nextLF(buf, ptr, '.'); + oldIds[ids.size()] = AbbreviatedObjectId.fromString(buf, ptr, dot2 - 1); + newId = AbbreviatedObjectId.fromString(buf, dot2 + 1, eol - 1); + oldModes = new FileMode[oldIds.length]; + } + + @Override + protected void parseNewFileMode(final int ptr, final int eol) { + for (int i = 0; i < oldModes.length; i++) + oldModes[i] = FileMode.MISSING; + super.parseNewFileMode(ptr, eol); + } + + @Override + HunkHeader newHunkHeader(final int offset) { + return new CombinedHunkHeader(this, offset); + } + + private void parseModeLine(int ptr, final int eol) { + // "mode $amode,$bmode..$cmode" + // + int n = 0; + while (ptr < eol) { + final int comma = nextLF(buf, ptr, ','); + if (eol <= comma) + break; + oldModes[n++] = parseFileMode(ptr, comma); + ptr = comma; + } + final int dot2 = nextLF(buf, ptr, '.'); + oldModes[n] = parseFileMode(ptr, dot2); + newMode = parseFileMode(dot2 + 1, eol); + } + + private void parseDeletedFileMode(int ptr, final int eol) { + // "deleted file mode $amode,$bmode" + // + changeType = ChangeType.DELETE; + int n = 0; + while (ptr < eol) { + final int comma = nextLF(buf, ptr, ','); + if (eol <= comma) + break; + oldModes[n++] = parseFileMode(ptr, comma); + ptr = comma; + } + oldModes[n] = parseFileMode(ptr, eol); + newMode = FileMode.MISSING; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java new file mode 100644 index 0000000000..781190539f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.patch; + +import static org.eclipse.jgit.util.RawParseUtils.nextLF; +import static org.eclipse.jgit.util.RawParseUtils.parseBase10; + +import java.io.IOException; +import java.io.OutputStream; + +import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.util.MutableInteger; + +/** Hunk header for a hunk appearing in a "diff --cc" style patch. */ +public class CombinedHunkHeader extends HunkHeader { + private static abstract class CombinedOldImage extends OldImage { + int nContext; + } + + private CombinedOldImage[] old; + + CombinedHunkHeader(final CombinedFileHeader fh, final int offset) { + super(fh, offset, null); + old = new CombinedOldImage[fh.getParentCount()]; + for (int i = 0; i < old.length; i++) { + final int imagePos = i; + old[i] = new CombinedOldImage() { + @Override + public AbbreviatedObjectId getId() { + return fh.getOldId(imagePos); + } + }; + } + } + + @Override + public CombinedFileHeader getFileHeader() { + return (CombinedFileHeader) super.getFileHeader(); + } + + @Override + public OldImage getOldImage() { + return getOldImage(0); + } + + /** + * Get the OldImage data related to the nth ancestor + * + * @param nthParent + * the ancestor to get the old image data of + * @return image data of the requested ancestor. + */ + public OldImage getOldImage(final int nthParent) { + return old[nthParent]; + } + + @Override + void parseHeader() { + // Parse "@@@ -55,12 -163,13 +163,15 @@@ protected boolean" + // + final byte[] buf = file.buf; + final MutableInteger ptr = new MutableInteger(); + ptr.value = nextLF(buf, startOffset, ' '); + + for (int n = 0; n < old.length; n++) { + old[n].startLine = -parseBase10(buf, ptr.value, ptr); + if (buf[ptr.value] == ',') + old[n].lineCount = parseBase10(buf, ptr.value + 1, ptr); + else + old[n].lineCount = 1; + } + + newStartLine = parseBase10(buf, ptr.value + 1, ptr); + if (buf[ptr.value] == ',') + newLineCount = parseBase10(buf, ptr.value + 1, ptr); + else + newLineCount = 1; + } + + @Override + int parseBody(final Patch script, final int end) { + final byte[] buf = file.buf; + int c = nextLF(buf, startOffset); + + for (final CombinedOldImage o : old) { + o.nDeleted = 0; + o.nAdded = 0; + o.nContext = 0; + } + nContext = 0; + int nAdded = 0; + + SCAN: for (int eol; c < end; c = eol) { + eol = nextLF(buf, c); + + if (eol - c < old.length + 1) { + // Line isn't long enough to mention the state of each + // ancestor. It must be the end of the hunk. + break SCAN; + } + + switch (buf[c]) { + case ' ': + case '-': + case '+': + break; + + default: + // Line can't possibly be part of this hunk; the first + // ancestor information isn't recognizable. + // + break SCAN; + } + + int localcontext = 0; + for (int ancestor = 0; ancestor < old.length; ancestor++) { + switch (buf[c + ancestor]) { + case ' ': + localcontext++; + old[ancestor].nContext++; + continue; + + case '-': + old[ancestor].nDeleted++; + continue; + + case '+': + old[ancestor].nAdded++; + nAdded++; + continue; + + default: + break SCAN; + } + } + if (localcontext == old.length) + nContext++; + } + + for (int ancestor = 0; ancestor < old.length; ancestor++) { + final CombinedOldImage o = old[ancestor]; + final int cmp = o.nContext + o.nDeleted; + if (cmp < o.lineCount) { + final int missingCnt = o.lineCount - cmp; + script.error(buf, startOffset, "Truncated hunk, at least " + + missingCnt + " lines is missing for ancestor " + + (ancestor + 1)); + } + } + + if (nContext + nAdded < newLineCount) { + final int missingCount = newLineCount - (nContext + nAdded); + script.error(buf, startOffset, "Truncated hunk, at least " + + missingCount + " new lines is missing"); + } + + return c; + } + + @Override + void extractFileLines(final OutputStream[] out) throws IOException { + final byte[] buf = file.buf; + int ptr = startOffset; + int eol = nextLF(buf, ptr); + if (endOffset <= eol) + return; + + // Treat the hunk header as though it were from the ancestor, + // as it may have a function header appearing after it which + // was copied out of the ancestor file. + // + out[0].write(buf, ptr, eol - ptr); + + SCAN: for (ptr = eol; ptr < endOffset; ptr = eol) { + eol = nextLF(buf, ptr); + + if (eol - ptr < old.length + 1) { + // Line isn't long enough to mention the state of each + // ancestor. It must be the end of the hunk. + break SCAN; + } + + switch (buf[ptr]) { + case ' ': + case '-': + case '+': + break; + + default: + // Line can't possibly be part of this hunk; the first + // ancestor information isn't recognizable. + // + break SCAN; + } + + int delcnt = 0; + for (int ancestor = 0; ancestor < old.length; ancestor++) { + switch (buf[ptr + ancestor]) { + case '-': + delcnt++; + out[ancestor].write(buf, ptr, eol - ptr); + continue; + + case ' ': + out[ancestor].write(buf, ptr, eol - ptr); + continue; + + case '+': + continue; + + default: + break SCAN; + } + } + if (delcnt < old.length) { + // This line appears in the new file if it wasn't deleted + // relative to all ancestors. + // + out[old.length].write(buf, ptr, eol - ptr); + } + } + } + + void extractFileLines(final StringBuilder sb, final String[] text, + final int[] offsets) { + final byte[] buf = file.buf; + int ptr = startOffset; + int eol = nextLF(buf, ptr); + if (endOffset <= eol) + return; + copyLine(sb, text, offsets, 0); + SCAN: for (ptr = eol; ptr < endOffset; ptr = eol) { + eol = nextLF(buf, ptr); + + if (eol - ptr < old.length + 1) { + // Line isn't long enough to mention the state of each + // ancestor. It must be the end of the hunk. + break SCAN; + } + + switch (buf[ptr]) { + case ' ': + case '-': + case '+': + break; + + default: + // Line can't possibly be part of this hunk; the first + // ancestor information isn't recognizable. + // + break SCAN; + } + + boolean copied = false; + for (int ancestor = 0; ancestor < old.length; ancestor++) { + switch (buf[ptr + ancestor]) { + case ' ': + case '-': + if (copied) + skipLine(text, offsets, ancestor); + else { + copyLine(sb, text, offsets, ancestor); + copied = true; + } + continue; + + case '+': + continue; + + default: + break SCAN; + } + } + if (!copied) { + // If none of the ancestors caused the copy then this line + // must be new across the board, so it only appears in the + // text of the new file. + // + copyLine(sb, text, offsets, old.length); + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java new file mode 100644 index 0000000000..dece17e4c8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java @@ -0,0 +1,714 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.patch; + +import static org.eclipse.jgit.lib.Constants.encodeASCII; +import static org.eclipse.jgit.util.RawParseUtils.decode; +import static org.eclipse.jgit.util.RawParseUtils.decodeNoFallback; +import static org.eclipse.jgit.util.RawParseUtils.extractBinaryString; +import static org.eclipse.jgit.util.RawParseUtils.match; +import static org.eclipse.jgit.util.RawParseUtils.nextLF; +import static org.eclipse.jgit.util.RawParseUtils.parseBase10; + +import java.io.IOException; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jgit.diff.EditList; +import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.util.QuotedString; +import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.TemporaryBuffer; + +/** Patch header describing an action for a single file path. */ +public class FileHeader { + /** Magical file name used for file adds or deletes. */ + public static final String DEV_NULL = "/dev/null"; + + private static final byte[] OLD_MODE = encodeASCII("old mode "); + + private static final byte[] NEW_MODE = encodeASCII("new mode "); + + static final byte[] DELETED_FILE_MODE = encodeASCII("deleted file mode "); + + static final byte[] NEW_FILE_MODE = encodeASCII("new file mode "); + + private static final byte[] COPY_FROM = encodeASCII("copy from "); + + private static final byte[] COPY_TO = encodeASCII("copy to "); + + private static final byte[] RENAME_OLD = encodeASCII("rename old "); + + private static final byte[] RENAME_NEW = encodeASCII("rename new "); + + private static final byte[] RENAME_FROM = encodeASCII("rename from "); + + private static final byte[] RENAME_TO = encodeASCII("rename to "); + + private static final byte[] SIMILARITY_INDEX = encodeASCII("similarity index "); + + private static final byte[] DISSIMILARITY_INDEX = encodeASCII("dissimilarity index "); + + static final byte[] INDEX = encodeASCII("index "); + + static final byte[] OLD_NAME = encodeASCII("--- "); + + static final byte[] NEW_NAME = encodeASCII("+++ "); + + /** General type of change a single file-level patch describes. */ + public static enum ChangeType { + /** Add a new file to the project */ + ADD, + + /** Modify an existing file in the project (content and/or mode) */ + MODIFY, + + /** Delete an existing file from the project */ + DELETE, + + /** Rename an existing file to a new location */ + RENAME, + + /** Copy an existing file to a new location, keeping the original */ + COPY; + } + + /** Type of patch used by this file. */ + public static enum PatchType { + /** A traditional unified diff style patch of a text file. */ + UNIFIED, + + /** An empty patch with a message "Binary files ... differ" */ + BINARY, + + /** A Git binary patch, holding pre and post image deltas */ + GIT_BINARY; + } + + /** Buffer holding the patch data for this file. */ + final byte[] buf; + + /** Offset within {@link #buf} to the "diff ..." line. */ + final int startOffset; + + /** Position 1 past the end of this file within {@link #buf}. */ + int endOffset; + + /** File name of the old (pre-image). */ + private String oldName; + + /** File name of the new (post-image). */ + private String newName; + + /** Old mode of the file, if described by the patch, else null. */ + private FileMode oldMode; + + /** New mode of the file, if described by the patch, else null. */ + protected FileMode newMode; + + /** General type of change indicated by the patch. */ + protected ChangeType changeType; + + /** Similarity score if {@link #changeType} is a copy or rename. */ + private int score; + + /** ObjectId listed on the index line for the old (pre-image) */ + private AbbreviatedObjectId oldId; + + /** ObjectId listed on the index line for the new (post-image) */ + protected AbbreviatedObjectId newId; + + /** Type of patch used to modify this file */ + PatchType patchType; + + /** The hunks of this file */ + private List<HunkHeader> hunks; + + /** If {@link #patchType} is {@link PatchType#GIT_BINARY}, the new image */ + BinaryHunk forwardBinaryHunk; + + /** If {@link #patchType} is {@link PatchType#GIT_BINARY}, the old image */ + BinaryHunk reverseBinaryHunk; + + FileHeader(final byte[] b, final int offset) { + buf = b; + startOffset = offset; + changeType = ChangeType.MODIFY; // unless otherwise designated + patchType = PatchType.UNIFIED; + } + + int getParentCount() { + return 1; + } + + /** @return the byte array holding this file's patch script. */ + public byte[] getBuffer() { + return buf; + } + + /** @return offset the start of this file's script in {@link #getBuffer()}. */ + public int getStartOffset() { + return startOffset; + } + + /** @return offset one past the end of the file script. */ + public int getEndOffset() { + return endOffset; + } + + /** + * Convert the patch script for this file into a string. + * <p> + * The default character encoding ({@link Constants#CHARSET}) is assumed for + * both the old and new files. + * + * @return the patch script, as a Unicode string. + */ + public String getScriptText() { + return getScriptText(null, null); + } + + /** + * Convert the patch script for this file into a string. + * + * @param oldCharset + * hint character set to decode the old lines with. + * @param newCharset + * hint character set to decode the new lines with. + * @return the patch script, as a Unicode string. + */ + public String getScriptText(Charset oldCharset, Charset newCharset) { + return getScriptText(new Charset[] { oldCharset, newCharset }); + } + + String getScriptText(Charset[] charsetGuess) { + if (getHunks().isEmpty()) { + // If we have no hunks then we can safely assume the entire + // patch is a binary style patch, or a meta-data only style + // patch. Either way the encoding of the headers should be + // strictly 7-bit US-ASCII and the body is either 7-bit ASCII + // (due to the base 85 encoding used for a BinaryHunk) or is + // arbitrary noise we have chosen to ignore and not understand + // (e.g. the message "Binary files ... differ"). + // + return extractBinaryString(buf, startOffset, endOffset); + } + + if (charsetGuess != null && charsetGuess.length != getParentCount() + 1) + throw new IllegalArgumentException("Expected " + + (getParentCount() + 1) + " character encoding guesses"); + + if (trySimpleConversion(charsetGuess)) { + Charset cs = charsetGuess != null ? charsetGuess[0] : null; + if (cs == null) + cs = Constants.CHARSET; + try { + return decodeNoFallback(cs, buf, startOffset, endOffset); + } catch (CharacterCodingException cee) { + // Try the much slower, more-memory intensive version which + // can handle a character set conversion patch. + } + } + + final StringBuilder r = new StringBuilder(endOffset - startOffset); + + // Always treat the headers as US-ASCII; Git file names are encoded + // in a C style escape if any character has the high-bit set. + // + final int hdrEnd = getHunks().get(0).getStartOffset(); + for (int ptr = startOffset; ptr < hdrEnd;) { + final int eol = Math.min(hdrEnd, nextLF(buf, ptr)); + r.append(extractBinaryString(buf, ptr, eol)); + ptr = eol; + } + + final String[] files = extractFileLines(charsetGuess); + final int[] offsets = new int[files.length]; + for (final HunkHeader h : getHunks()) + h.extractFileLines(r, files, offsets); + return r.toString(); + } + + private static boolean trySimpleConversion(final Charset[] charsetGuess) { + if (charsetGuess == null) + return true; + for (int i = 1; i < charsetGuess.length; i++) { + if (charsetGuess[i] != charsetGuess[0]) + return false; + } + return true; + } + + private String[] extractFileLines(final Charset[] csGuess) { + final TemporaryBuffer[] tmp = new TemporaryBuffer[getParentCount() + 1]; + try { + for (int i = 0; i < tmp.length; i++) + tmp[i] = new TemporaryBuffer(); + for (final HunkHeader h : getHunks()) + h.extractFileLines(tmp); + + final String[] r = new String[tmp.length]; + for (int i = 0; i < tmp.length; i++) { + Charset cs = csGuess != null ? csGuess[i] : null; + if (cs == null) + cs = Constants.CHARSET; + r[i] = RawParseUtils.decode(cs, tmp[i].toByteArray()); + } + return r; + } catch (IOException ioe) { + throw new RuntimeException("Cannot convert script to text", ioe); + } finally { + for (final TemporaryBuffer b : tmp) { + if (b != null) + b.destroy(); + } + } + } + + /** + * Get the old name associated with this file. + * <p> + * The meaning of the old name can differ depending on the semantic meaning + * of this patch: + * <ul> + * <li><i>file add</i>: always <code>/dev/null</code></li> + * <li><i>file modify</i>: always {@link #getNewName()}</li> + * <li><i>file delete</i>: always the file being deleted</li> + * <li><i>file copy</i>: source file the copy originates from</li> + * <li><i>file rename</i>: source file the rename originates from</li> + * </ul> + * + * @return old name for this file. + */ + public String getOldName() { + return oldName; + } + + /** + * Get the new name associated with this file. + * <p> + * The meaning of the new name can differ depending on the semantic meaning + * of this patch: + * <ul> + * <li><i>file add</i>: always the file being created</li> + * <li><i>file modify</i>: always {@link #getOldName()}</li> + * <li><i>file delete</i>: always <code>/dev/null</code></li> + * <li><i>file copy</i>: destination file the copy ends up at</li> + * <li><i>file rename</i>: destination file the rename ends up at/li> + * </ul> + * + * @return new name for this file. + */ + public String getNewName() { + return newName; + } + + /** @return the old file mode, if described in the patch */ + public FileMode getOldMode() { + return oldMode; + } + + /** @return the new file mode, if described in the patch */ + public FileMode getNewMode() { + return newMode; + } + + /** @return the type of change this patch makes on {@link #getNewName()} */ + public ChangeType getChangeType() { + return changeType; + } + + /** + * @return similarity score between {@link #getOldName()} and + * {@link #getNewName()} if {@link #getChangeType()} is + * {@link ChangeType#COPY} or {@link ChangeType#RENAME}. + */ + public int getScore() { + return score; + } + + /** + * Get the old object id from the <code>index</code>. + * + * @return the object id; null if there is no index line + */ + public AbbreviatedObjectId getOldId() { + return oldId; + } + + /** + * Get the new object id from the <code>index</code>. + * + * @return the object id; null if there is no index line + */ + public AbbreviatedObjectId getNewId() { + return newId; + } + + /** @return style of patch used to modify this file */ + public PatchType getPatchType() { + return patchType; + } + + /** @return true if this patch modifies metadata about a file */ + public boolean hasMetaDataChanges() { + return changeType != ChangeType.MODIFY || newMode != oldMode; + } + + /** @return hunks altering this file; in order of appearance in patch */ + public List<? extends HunkHeader> getHunks() { + if (hunks == null) + return Collections.emptyList(); + return hunks; + } + + void addHunk(final HunkHeader h) { + if (h.getFileHeader() != this) + throw new IllegalArgumentException("Hunk belongs to another file"); + if (hunks == null) + hunks = new ArrayList<HunkHeader>(); + hunks.add(h); + } + + HunkHeader newHunkHeader(final int offset) { + return new HunkHeader(this, offset); + } + + /** @return if a {@link PatchType#GIT_BINARY}, the new-image delta/literal */ + public BinaryHunk getForwardBinaryHunk() { + return forwardBinaryHunk; + } + + /** @return if a {@link PatchType#GIT_BINARY}, the old-image delta/literal */ + public BinaryHunk getReverseBinaryHunk() { + return reverseBinaryHunk; + } + + /** @return a list describing the content edits performed on this file. */ + public EditList toEditList() { + final EditList r = new EditList(); + for (final HunkHeader hunk : hunks) + r.addAll(hunk.toEditList()); + return r; + } + + /** + * Parse a "diff --git" or "diff --cc" line. + * + * @param ptr + * first character after the "diff --git " or "diff --cc " part. + * @param end + * one past the last position to parse. + * @return first character after the LF at the end of the line; -1 on error. + */ + int parseGitFileName(int ptr, final int end) { + final int eol = nextLF(buf, ptr); + final int bol = ptr; + if (eol >= end) { + return -1; + } + + // buffer[ptr..eol] looks like "a/foo b/foo\n". After the first + // A regex to match this is "^[^/]+/(.*?) [^/+]+/\1\n$". There + // is only one way to split the line such that text to the left + // of the space matches the text to the right, excluding the part + // before the first slash. + // + + final int aStart = nextLF(buf, ptr, '/'); + if (aStart >= eol) + return eol; + + while (ptr < eol) { + final int sp = nextLF(buf, ptr, ' '); + if (sp >= eol) { + // We can't split the header, it isn't valid. + // This may be OK if this is a rename patch. + // + return eol; + } + final int bStart = nextLF(buf, sp, '/'); + if (bStart >= eol) + return eol; + + // If buffer[aStart..sp - 1] = buffer[bStart..eol - 1] + // we have a valid split. + // + if (eq(aStart, sp - 1, bStart, eol - 1)) { + if (buf[bol] == '"') { + // We're a double quoted name. The region better end + // in a double quote too, and we need to decode the + // characters before reading the name. + // + if (buf[sp - 2] != '"') { + return eol; + } + oldName = QuotedString.GIT_PATH.dequote(buf, bol, sp - 1); + oldName = p1(oldName); + } else { + oldName = decode(Constants.CHARSET, buf, aStart, sp - 1); + } + newName = oldName; + return eol; + } + + // This split wasn't correct. Move past the space and try + // another split as the space must be part of the file name. + // + ptr = sp; + } + + return eol; + } + + int parseGitHeaders(int ptr, final int end) { + while (ptr < end) { + final int eol = nextLF(buf, ptr); + if (isHunkHdr(buf, ptr, eol) >= 1) { + // First hunk header; break out and parse them later. + break; + + } else if (match(buf, ptr, OLD_NAME) >= 0) { + parseOldName(ptr, eol); + + } else if (match(buf, ptr, NEW_NAME) >= 0) { + parseNewName(ptr, eol); + + } else if (match(buf, ptr, OLD_MODE) >= 0) { + oldMode = parseFileMode(ptr + OLD_MODE.length, eol); + + } else if (match(buf, ptr, NEW_MODE) >= 0) { + newMode = parseFileMode(ptr + NEW_MODE.length, eol); + + } else if (match(buf, ptr, DELETED_FILE_MODE) >= 0) { + oldMode = parseFileMode(ptr + DELETED_FILE_MODE.length, eol); + newMode = FileMode.MISSING; + changeType = ChangeType.DELETE; + + } else if (match(buf, ptr, NEW_FILE_MODE) >= 0) { + parseNewFileMode(ptr, eol); + + } else if (match(buf, ptr, COPY_FROM) >= 0) { + oldName = parseName(oldName, ptr + COPY_FROM.length, eol); + changeType = ChangeType.COPY; + + } else if (match(buf, ptr, COPY_TO) >= 0) { + newName = parseName(newName, ptr + COPY_TO.length, eol); + changeType = ChangeType.COPY; + + } else if (match(buf, ptr, RENAME_OLD) >= 0) { + oldName = parseName(oldName, ptr + RENAME_OLD.length, eol); + changeType = ChangeType.RENAME; + + } else if (match(buf, ptr, RENAME_NEW) >= 0) { + newName = parseName(newName, ptr + RENAME_NEW.length, eol); + changeType = ChangeType.RENAME; + + } else if (match(buf, ptr, RENAME_FROM) >= 0) { + oldName = parseName(oldName, ptr + RENAME_FROM.length, eol); + changeType = ChangeType.RENAME; + + } else if (match(buf, ptr, RENAME_TO) >= 0) { + newName = parseName(newName, ptr + RENAME_TO.length, eol); + changeType = ChangeType.RENAME; + + } else if (match(buf, ptr, SIMILARITY_INDEX) >= 0) { + score = parseBase10(buf, ptr + SIMILARITY_INDEX.length, null); + + } else if (match(buf, ptr, DISSIMILARITY_INDEX) >= 0) { + score = parseBase10(buf, ptr + DISSIMILARITY_INDEX.length, null); + + } else if (match(buf, ptr, INDEX) >= 0) { + parseIndexLine(ptr + INDEX.length, eol); + + } else { + // Probably an empty patch (stat dirty). + break; + } + + ptr = eol; + } + return ptr; + } + + void parseOldName(int ptr, final int eol) { + oldName = p1(parseName(oldName, ptr + OLD_NAME.length, eol)); + if (oldName == DEV_NULL) + changeType = ChangeType.ADD; + } + + void parseNewName(int ptr, final int eol) { + newName = p1(parseName(newName, ptr + NEW_NAME.length, eol)); + if (newName == DEV_NULL) + changeType = ChangeType.DELETE; + } + + void parseNewFileMode(int ptr, final int eol) { + oldMode = FileMode.MISSING; + newMode = parseFileMode(ptr + NEW_FILE_MODE.length, eol); + changeType = ChangeType.ADD; + } + + int parseTraditionalHeaders(int ptr, final int end) { + while (ptr < end) { + final int eol = nextLF(buf, ptr); + if (isHunkHdr(buf, ptr, eol) >= 1) { + // First hunk header; break out and parse them later. + break; + + } else if (match(buf, ptr, OLD_NAME) >= 0) { + parseOldName(ptr, eol); + + } else if (match(buf, ptr, NEW_NAME) >= 0) { + parseNewName(ptr, eol); + + } else { + // Possibly an empty patch. + break; + } + + ptr = eol; + } + return ptr; + } + + private String parseName(final String expect, int ptr, final int end) { + if (ptr == end) + return expect; + + String r; + if (buf[ptr] == '"') { + // New style GNU diff format + // + r = QuotedString.GIT_PATH.dequote(buf, ptr, end - 1); + } else { + // Older style GNU diff format, an optional tab ends the name. + // + int tab = end; + while (ptr < tab && buf[tab - 1] != '\t') + tab--; + if (ptr == tab) + tab = end; + r = decode(Constants.CHARSET, buf, ptr, tab - 1); + } + + if (r.equals(DEV_NULL)) + r = DEV_NULL; + return r; + } + + private static String p1(final String r) { + final int s = r.indexOf('/'); + return s > 0 ? r.substring(s + 1) : r; + } + + FileMode parseFileMode(int ptr, final int end) { + int tmp = 0; + while (ptr < end - 1) { + tmp <<= 3; + tmp += buf[ptr++] - '0'; + } + return FileMode.fromBits(tmp); + } + + void parseIndexLine(int ptr, final int end) { + // "index $asha1..$bsha1[ $mode]" where $asha1 and $bsha1 + // can be unique abbreviations + // + final int dot2 = nextLF(buf, ptr, '.'); + final int mode = nextLF(buf, dot2, ' '); + + oldId = AbbreviatedObjectId.fromString(buf, ptr, dot2 - 1); + newId = AbbreviatedObjectId.fromString(buf, dot2 + 1, mode - 1); + + if (mode < end) + newMode = oldMode = parseFileMode(mode, end); + } + + private boolean eq(int aPtr, int aEnd, int bPtr, int bEnd) { + if (aEnd - aPtr != bEnd - bPtr) { + return false; + } + while (aPtr < aEnd) { + if (buf[aPtr++] != buf[bPtr++]) + return false; + } + return true; + } + + /** + * Determine if this is a patch hunk header. + * + * @param buf + * the buffer to scan + * @param start + * first position in the buffer to evaluate + * @param end + * last position to consider; usually the end of the buffer ( + * <code>buf.length</code>) or the first position on the next + * line. This is only used to avoid very long runs of '@' from + * killing the scan loop. + * @return the number of "ancestor revisions" in the hunk header. A + * traditional two-way diff ("@@ -...") returns 1; a combined diff + * for a 3 way-merge returns 3. If this is not a hunk header, 0 is + * returned instead. + */ + static int isHunkHdr(final byte[] buf, final int start, final int end) { + int ptr = start; + while (ptr < end && buf[ptr] == '@') + ptr++; + if (ptr - start < 2) + return 0; + if (ptr == end || buf[ptr++] != ' ') + return 0; + if (ptr == end || buf[ptr++] != '-') + return 0; + return (ptr - 3) - start; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java new file mode 100644 index 0000000000..13046137dc --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.patch; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.util.RawParseUtils; + +/** An error in a patch script */ +public class FormatError { + /** Classification of an error. */ + public static enum Severity { + /** The error is unexpected, but can be worked around. */ + WARNING, + + /** The error indicates the script is severely flawed. */ + ERROR; + } + + private final byte[] buf; + + private final int offset; + + private final Severity severity; + + private final String message; + + FormatError(final byte[] buffer, final int ptr, final Severity sev, + final String msg) { + buf = buffer; + offset = ptr; + severity = sev; + message = msg; + } + + /** @return the severity of the error. */ + public Severity getSeverity() { + return severity; + } + + /** @return a message describing the error. */ + public String getMessage() { + return message; + } + + /** @return the byte buffer holding the patch script. */ + public byte[] getBuffer() { + return buf; + } + + /** @return byte offset within {@link #getBuffer()} where the error is */ + public int getOffset() { + return offset; + } + + /** @return line of the patch script the error appears on. */ + public String getLineText() { + final int eol = RawParseUtils.nextLF(buf, offset); + return RawParseUtils.decode(Constants.CHARSET, buf, offset, eol); + } + + @Override + public String toString() { + final StringBuilder r = new StringBuilder(); + r.append(getSeverity().name().toLowerCase()); + r.append(": at offset "); + r.append(getOffset()); + r.append(": "); + r.append(getMessage()); + r.append("\n"); + r.append(" in "); + r.append(getLineText()); + return r.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java new file mode 100644 index 0000000000..9d78d0b99f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.patch; + +import static org.eclipse.jgit.util.RawParseUtils.match; +import static org.eclipse.jgit.util.RawParseUtils.nextLF; +import static org.eclipse.jgit.util.RawParseUtils.parseBase10; + +import java.io.IOException; +import java.io.OutputStream; + +import org.eclipse.jgit.diff.Edit; +import org.eclipse.jgit.diff.EditList; +import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.util.MutableInteger; + +/** Hunk header describing the layout of a single block of lines */ +public class HunkHeader { + /** Details about an old image of the file. */ + public abstract static class OldImage { + /** First line number the hunk starts on in this file. */ + int startLine; + + /** Total number of lines this hunk covers in this file. */ + int lineCount; + + /** Number of lines deleted by the post-image from this file. */ + int nDeleted; + + /** Number of lines added by the post-image not in this file. */ + int nAdded; + + /** @return first line number the hunk starts on in this file. */ + public int getStartLine() { + return startLine; + } + + /** @return total number of lines this hunk covers in this file. */ + public int getLineCount() { + return lineCount; + } + + /** @return number of lines deleted by the post-image from this file. */ + public int getLinesDeleted() { + return nDeleted; + } + + /** @return number of lines added by the post-image not in this file. */ + public int getLinesAdded() { + return nAdded; + } + + /** @return object id of the pre-image file. */ + public abstract AbbreviatedObjectId getId(); + } + + final FileHeader file; + + /** Offset within {@link #file}.buf to the "@@ -" line. */ + final int startOffset; + + /** Position 1 past the end of this hunk within {@link #file}'s buf. */ + int endOffset; + + private final OldImage old; + + /** First line number in the post-image file where the hunk starts */ + int newStartLine; + + /** Total number of post-image lines this hunk covers (context + inserted) */ + int newLineCount; + + /** Total number of lines of context appearing in this hunk */ + int nContext; + + HunkHeader(final FileHeader fh, final int offset) { + this(fh, offset, new OldImage() { + @Override + public AbbreviatedObjectId getId() { + return fh.getOldId(); + } + }); + } + + HunkHeader(final FileHeader fh, final int offset, final OldImage oi) { + file = fh; + startOffset = offset; + old = oi; + } + + /** @return header for the file this hunk applies to */ + public FileHeader getFileHeader() { + return file; + } + + /** @return the byte array holding this hunk's patch script. */ + public byte[] getBuffer() { + return file.buf; + } + + /** @return offset the start of this hunk in {@link #getBuffer()}. */ + public int getStartOffset() { + return startOffset; + } + + /** @return offset one past the end of the hunk in {@link #getBuffer()}. */ + public int getEndOffset() { + return endOffset; + } + + /** @return information about the old image mentioned in this hunk. */ + public OldImage getOldImage() { + return old; + } + + /** @return first line number in the post-image file where the hunk starts */ + public int getNewStartLine() { + return newStartLine; + } + + /** @return Total number of post-image lines this hunk covers */ + public int getNewLineCount() { + return newLineCount; + } + + /** @return total number of lines of context appearing in this hunk */ + public int getLinesContext() { + return nContext; + } + + /** @return a list describing the content edits performed within the hunk. */ + public EditList toEditList() { + final EditList r = new EditList(); + final byte[] buf = file.buf; + int c = nextLF(buf, startOffset); + int oLine = old.startLine; + int nLine = newStartLine; + Edit in = null; + + SCAN: for (; c < endOffset; c = nextLF(buf, c)) { + switch (buf[c]) { + case ' ': + case '\n': + in = null; + oLine++; + nLine++; + continue; + + case '-': + if (in == null) { + in = new Edit(oLine - 1, nLine - 1); + r.add(in); + } + oLine++; + in.extendA(); + continue; + + case '+': + if (in == null) { + in = new Edit(oLine - 1, nLine - 1); + r.add(in); + } + nLine++; + in.extendB(); + continue; + + case '\\': // Matches "\ No newline at end of file" + continue; + + default: + break SCAN; + } + } + return r; + } + + void parseHeader() { + // Parse "@@ -236,9 +236,9 @@ protected boolean" + // + final byte[] buf = file.buf; + final MutableInteger ptr = new MutableInteger(); + ptr.value = nextLF(buf, startOffset, ' '); + old.startLine = -parseBase10(buf, ptr.value, ptr); + if (buf[ptr.value] == ',') + old.lineCount = parseBase10(buf, ptr.value + 1, ptr); + else + old.lineCount = 1; + + newStartLine = parseBase10(buf, ptr.value + 1, ptr); + if (buf[ptr.value] == ',') + newLineCount = parseBase10(buf, ptr.value + 1, ptr); + else + newLineCount = 1; + } + + int parseBody(final Patch script, final int end) { + final byte[] buf = file.buf; + int c = nextLF(buf, startOffset), last = c; + + old.nDeleted = 0; + old.nAdded = 0; + + SCAN: for (; c < end; last = c, c = nextLF(buf, c)) { + switch (buf[c]) { + case ' ': + case '\n': + nContext++; + continue; + + case '-': + old.nDeleted++; + continue; + + case '+': + old.nAdded++; + continue; + + case '\\': // Matches "\ No newline at end of file" + continue; + + default: + break SCAN; + } + } + + if (last < end && nContext + old.nDeleted - 1 == old.lineCount + && nContext + old.nAdded == newLineCount + && match(buf, last, Patch.SIG_FOOTER) >= 0) { + // This is an extremely common occurrence of "corruption". + // Users add footers with their signatures after this mark, + // and git diff adds the git executable version number. + // Let it slide; the hunk otherwise looked sound. + // + old.nDeleted--; + return last; + } + + if (nContext + old.nDeleted < old.lineCount) { + final int missingCount = old.lineCount - (nContext + old.nDeleted); + script.error(buf, startOffset, "Truncated hunk, at least " + + missingCount + " old lines is missing"); + + } else if (nContext + old.nAdded < newLineCount) { + final int missingCount = newLineCount - (nContext + old.nAdded); + script.error(buf, startOffset, "Truncated hunk, at least " + + missingCount + " new lines is missing"); + + } else if (nContext + old.nDeleted > old.lineCount + || nContext + old.nAdded > newLineCount) { + final String oldcnt = old.lineCount + ":" + newLineCount; + final String newcnt = (nContext + old.nDeleted) + ":" + + (nContext + old.nAdded); + script.warn(buf, startOffset, "Hunk header " + oldcnt + + " does not match body line count of " + newcnt); + } + + return c; + } + + void extractFileLines(final OutputStream[] out) throws IOException { + final byte[] buf = file.buf; + int ptr = startOffset; + int eol = nextLF(buf, ptr); + if (endOffset <= eol) + return; + + // Treat the hunk header as though it were from the ancestor, + // as it may have a function header appearing after it which + // was copied out of the ancestor file. + // + out[0].write(buf, ptr, eol - ptr); + + SCAN: for (ptr = eol; ptr < endOffset; ptr = eol) { + eol = nextLF(buf, ptr); + switch (buf[ptr]) { + case ' ': + case '\n': + case '\\': + out[0].write(buf, ptr, eol - ptr); + out[1].write(buf, ptr, eol - ptr); + break; + case '-': + out[0].write(buf, ptr, eol - ptr); + break; + case '+': + out[1].write(buf, ptr, eol - ptr); + break; + default: + break SCAN; + } + } + } + + void extractFileLines(final StringBuilder sb, final String[] text, + final int[] offsets) { + final byte[] buf = file.buf; + int ptr = startOffset; + int eol = nextLF(buf, ptr); + if (endOffset <= eol) + return; + copyLine(sb, text, offsets, 0); + SCAN: for (ptr = eol; ptr < endOffset; ptr = eol) { + eol = nextLF(buf, ptr); + switch (buf[ptr]) { + case ' ': + case '\n': + case '\\': + copyLine(sb, text, offsets, 0); + skipLine(text, offsets, 1); + break; + case '-': + copyLine(sb, text, offsets, 0); + break; + case '+': + copyLine(sb, text, offsets, 1); + break; + default: + break SCAN; + } + } + } + + void copyLine(final StringBuilder sb, final String[] text, + final int[] offsets, final int fileIdx) { + final String s = text[fileIdx]; + final int start = offsets[fileIdx]; + int end = s.indexOf('\n', start); + if (end < 0) + end = s.length(); + else + end++; + sb.append(s, start, end); + offsets[fileIdx] = end; + } + + void skipLine(final String[] text, final int[] offsets, + final int fileIdx) { + final String s = text[fileIdx]; + final int end = s.indexOf('\n', offsets[fileIdx]); + offsets[fileIdx] = end < 0 ? s.length() : end + 1; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/Patch.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/Patch.java new file mode 100644 index 0000000000..1eff3edd8c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/Patch.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.patch; + +import static org.eclipse.jgit.lib.Constants.encodeASCII; +import static org.eclipse.jgit.patch.FileHeader.isHunkHdr; +import static org.eclipse.jgit.patch.FileHeader.NEW_NAME; +import static org.eclipse.jgit.patch.FileHeader.OLD_NAME; +import static org.eclipse.jgit.util.RawParseUtils.match; +import static org.eclipse.jgit.util.RawParseUtils.nextLF; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jgit.util.TemporaryBuffer; + +/** A parsed collection of {@link FileHeader}s from a unified diff patch file */ +public class Patch { + private static final byte[] DIFF_GIT = encodeASCII("diff --git "); + + private static final byte[] DIFF_CC = encodeASCII("diff --cc "); + + private static final byte[] DIFF_COMBINED = encodeASCII("diff --combined "); + + private static final byte[][] BIN_HEADERS = new byte[][] { + encodeASCII("Binary files "), encodeASCII("Files "), }; + + private static final byte[] BIN_TRAILER = encodeASCII(" differ\n"); + + private static final byte[] GIT_BINARY = encodeASCII("GIT binary patch\n"); + + static final byte[] SIG_FOOTER = encodeASCII("-- \n"); + + /** The files, in the order they were parsed out of the input. */ + private final List<FileHeader> files; + + /** Formatting errors, if any were identified. */ + private final List<FormatError> errors; + + /** Create an empty patch. */ + public Patch() { + files = new ArrayList<FileHeader>(); + errors = new ArrayList<FormatError>(0); + } + + /** + * Add a single file to this patch. + * <p> + * Typically files should be added by parsing the text through one of this + * class's parse methods. + * + * @param fh + * the header of the file. + */ + public void addFile(final FileHeader fh) { + files.add(fh); + } + + /** @return list of files described in the patch, in occurrence order. */ + public List<? extends FileHeader> getFiles() { + return files; + } + + /** + * Add a formatting error to this patch script. + * + * @param err + * the error description. + */ + public void addError(final FormatError err) { + errors.add(err); + } + + /** @return collection of formatting errors, if any. */ + public List<FormatError> getErrors() { + return errors; + } + + /** + * Parse a patch received from an InputStream. + * <p> + * Multiple parse calls on the same instance will concatenate the patch + * data, but each parse input must start with a valid file header (don't + * split a single file across parse calls). + * + * @param is + * the stream to read the patch data from. The stream is read + * until EOF is reached. + * @throws IOException + * there was an error reading from the input stream. + */ + public void parse(final InputStream is) throws IOException { + final byte[] buf = readFully(is); + parse(buf, 0, buf.length); + } + + private static byte[] readFully(final InputStream is) throws IOException { + final TemporaryBuffer b = new TemporaryBuffer(); + try { + b.copy(is); + b.close(); + return b.toByteArray(); + } finally { + b.destroy(); + } + } + + /** + * Parse a patch stored in a byte[]. + * <p> + * Multiple parse calls on the same instance will concatenate the patch + * data, but each parse input must start with a valid file header (don't + * split a single file across parse calls). + * + * @param buf + * the buffer to parse. + * @param ptr + * starting position to parse from. + * @param end + * 1 past the last position to end parsing. The total length to + * be parsed is <code>end - ptr</code>. + */ + public void parse(final byte[] buf, int ptr, final int end) { + while (ptr < end) + ptr = parseFile(buf, ptr, end); + } + + private int parseFile(final byte[] buf, int c, final int end) { + while (c < end) { + if (isHunkHdr(buf, c, end) >= 1) { + // If we find a disconnected hunk header we might + // have missed a file header previously. The hunk + // isn't valid without knowing where it comes from. + // + error(buf, c, "Hunk disconnected from file"); + c = nextLF(buf, c); + continue; + } + + // Valid git style patch? + // + if (match(buf, c, DIFF_GIT) >= 0) + return parseDiffGit(buf, c, end); + if (match(buf, c, DIFF_CC) >= 0) + return parseDiffCombined(DIFF_CC, buf, c, end); + if (match(buf, c, DIFF_COMBINED) >= 0) + return parseDiffCombined(DIFF_COMBINED, buf, c, end); + + // Junk between files? Leading junk? Traditional + // (non-git generated) patch? + // + final int n = nextLF(buf, c); + if (n >= end) { + // Patches cannot be only one line long. This must be + // trailing junk that we should ignore. + // + return end; + } + + if (n - c < 6) { + // A valid header must be at least 6 bytes on the + // first line, e.g. "--- a/b\n". + // + c = n; + continue; + } + + if (match(buf, c, OLD_NAME) >= 0 && match(buf, n, NEW_NAME) >= 0) { + // Probably a traditional patch. Ensure we have at least + // a "@@ -0,0" smelling line next. We only check the "@@ -". + // + final int f = nextLF(buf, n); + if (f >= end) + return end; + if (isHunkHdr(buf, f, end) == 1) + return parseTraditionalPatch(buf, c, end); + } + + c = n; + } + return c; + } + + private int parseDiffGit(final byte[] buf, final int start, final int end) { + final FileHeader fh = new FileHeader(buf, start); + int ptr = fh.parseGitFileName(start + DIFF_GIT.length, end); + if (ptr < 0) + return skipFile(buf, start); + + ptr = fh.parseGitHeaders(ptr, end); + ptr = parseHunks(fh, ptr, end); + fh.endOffset = ptr; + addFile(fh); + return ptr; + } + + private int parseDiffCombined(final byte[] hdr, final byte[] buf, + final int start, final int end) { + final CombinedFileHeader fh = new CombinedFileHeader(buf, start); + int ptr = fh.parseGitFileName(start + hdr.length, end); + if (ptr < 0) + return skipFile(buf, start); + + ptr = fh.parseGitHeaders(ptr, end); + ptr = parseHunks(fh, ptr, end); + fh.endOffset = ptr; + addFile(fh); + return ptr; + } + + private int parseTraditionalPatch(final byte[] buf, final int start, + final int end) { + final FileHeader fh = new FileHeader(buf, start); + int ptr = fh.parseTraditionalHeaders(start, end); + ptr = parseHunks(fh, ptr, end); + fh.endOffset = ptr; + addFile(fh); + return ptr; + } + + private static int skipFile(final byte[] buf, int ptr) { + ptr = nextLF(buf, ptr); + if (match(buf, ptr, OLD_NAME) >= 0) + ptr = nextLF(buf, ptr); + return ptr; + } + + private int parseHunks(final FileHeader fh, int c, final int end) { + final byte[] buf = fh.buf; + while (c < end) { + // If we see a file header at this point, we have all of the + // hunks for our current file. We should stop and report back + // with this position so it can be parsed again later. + // + if (match(buf, c, DIFF_GIT) >= 0) + break; + if (match(buf, c, DIFF_CC) >= 0) + break; + if (match(buf, c, DIFF_COMBINED) >= 0) + break; + if (match(buf, c, OLD_NAME) >= 0) + break; + if (match(buf, c, NEW_NAME) >= 0) + break; + + if (isHunkHdr(buf, c, end) == fh.getParentCount()) { + final HunkHeader h = fh.newHunkHeader(c); + h.parseHeader(); + c = h.parseBody(this, end); + h.endOffset = c; + fh.addHunk(h); + if (c < end) { + switch (buf[c]) { + case '@': + case 'd': + case '\n': + break; + default: + if (match(buf, c, SIG_FOOTER) < 0) + warn(buf, c, "Unexpected hunk trailer"); + } + } + continue; + } + + final int eol = nextLF(buf, c); + if (fh.getHunks().isEmpty() && match(buf, c, GIT_BINARY) >= 0) { + fh.patchType = FileHeader.PatchType.GIT_BINARY; + return parseGitBinary(fh, eol, end); + } + + if (fh.getHunks().isEmpty() && BIN_TRAILER.length < eol - c + && match(buf, eol - BIN_TRAILER.length, BIN_TRAILER) >= 0 + && matchAny(buf, c, BIN_HEADERS)) { + // The patch is a binary file diff, with no deltas. + // + fh.patchType = FileHeader.PatchType.BINARY; + return eol; + } + + // Skip this line and move to the next. Its probably garbage + // after the last hunk of a file. + // + c = eol; + } + + if (fh.getHunks().isEmpty() + && fh.getPatchType() == FileHeader.PatchType.UNIFIED + && !fh.hasMetaDataChanges()) { + // Hmm, an empty patch? If there is no metadata here we + // really have a binary patch that we didn't notice above. + // + fh.patchType = FileHeader.PatchType.BINARY; + } + + return c; + } + + private int parseGitBinary(final FileHeader fh, int c, final int end) { + final BinaryHunk postImage = new BinaryHunk(fh, c); + final int nEnd = postImage.parseHunk(c, end); + if (nEnd < 0) { + // Not a binary hunk. + // + error(fh.buf, c, "Missing forward-image in GIT binary patch"); + return c; + } + c = nEnd; + postImage.endOffset = c; + fh.forwardBinaryHunk = postImage; + + final BinaryHunk preImage = new BinaryHunk(fh, c); + final int oEnd = preImage.parseHunk(c, end); + if (oEnd >= 0) { + c = oEnd; + preImage.endOffset = c; + fh.reverseBinaryHunk = preImage; + } + + return c; + } + + void warn(final byte[] buf, final int ptr, final String msg) { + addError(new FormatError(buf, ptr, FormatError.Severity.WARNING, msg)); + } + + void error(final byte[] buf, final int ptr, final String msg) { + addError(new FormatError(buf, ptr, FormatError.Severity.ERROR, msg)); + } + + private static boolean matchAny(final byte[] buf, final int c, + final byte[][] srcs) { + for (final byte[] s : srcs) { + if (match(buf, c, s) >= 0) + return true; + } + return false; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/AbstractPlotRenderer.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/AbstractPlotRenderer.java new file mode 100644 index 0000000000..f872ae0a40 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/AbstractPlotRenderer.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revplot; + +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revwalk.RevFlag; + +/** + * Basic commit graph renderer for graphical user interfaces. + * <p> + * Lanes are drawn as columns left-to-right in the graph, and the commit short + * message is drawn to the right of the lane lines for this cell. It is assumed + * that the commits are being drawn as rows of some sort of table. + * <p> + * Client applications can subclass this implementation to provide the necessary + * drawing primitives required to display a commit graph. Most of the graph + * layout is handled by this class, allowing applications to implement only a + * handful of primitive stubs. + * <p> + * This class is suitable for us within an AWT TableCellRenderer or within a SWT + * PaintListener registered on a Table instance. It is meant to rubber stamp the + * graphics necessary for one row of a plotted commit list. + * <p> + * Subclasses should call {@link #paintCommit(PlotCommit, int)} after they have + * otherwise configured their instance to draw one commit into the current + * location. + * <p> + * All drawing methods assume the coordinate space for the current commit's cell + * starts at (upper left corner is) 0,0. If this is not true (like say in SWT) + * the implementation must perform the cell offset computations within the + * various draw methods. + * + * @param <TLane> + * type of lane being used by the application. + * @param <TColor> + * type of color object used by the graphics library. + */ +public abstract class AbstractPlotRenderer<TLane extends PlotLane, TColor> { + private static final int LANE_WIDTH = 14; + + private static final int LINE_WIDTH = 2; + + private static final int LEFT_PAD = 2; + + /** + * Paint one commit using the underlying graphics library. + * + * @param commit + * the commit to render in this cell. Must not be null. + * @param h + * total height (in pixels) of this cell. + */ + protected void paintCommit(final PlotCommit<TLane> commit, final int h) { + final int dotSize = computeDotSize(h); + final TLane myLane = commit.getLane(); + final int myLaneX = laneC(myLane); + final TColor myColor = laneColor(myLane); + + int maxCenter = 0; + for (final TLane passingLane : (TLane[]) commit.passingLanes) { + final int cx = laneC(passingLane); + final TColor c = laneColor(passingLane); + drawLine(c, cx, 0, cx, h, LINE_WIDTH); + maxCenter = Math.max(maxCenter, cx); + } + + final int nParent = commit.getParentCount(); + for (int i = 0; i < nParent; i++) { + final PlotCommit<TLane> p; + final TLane pLane; + final TColor pColor; + final int cx; + + p = (PlotCommit<TLane>) commit.getParent(i); + pLane = p.getLane(); + if (pLane == null) + continue; + + pColor = laneColor(pLane); + cx = laneC(pLane); + + if (Math.abs(myLaneX - cx) > LANE_WIDTH) { + if (myLaneX < cx) { + final int ix = cx - LANE_WIDTH / 2; + drawLine(pColor, myLaneX, h / 2, ix, h / 2, LINE_WIDTH); + drawLine(pColor, ix, h / 2, cx, h, LINE_WIDTH); + } else { + final int ix = cx + LANE_WIDTH / 2; + drawLine(pColor, myLaneX, h / 2, ix, h / 2, LINE_WIDTH); + drawLine(pColor, ix, h / 2, cx, h, LINE_WIDTH); + } + } else { + drawLine(pColor, myLaneX, h / 2, cx, h, LINE_WIDTH); + } + maxCenter = Math.max(maxCenter, cx); + } + + final int dotX = myLaneX - dotSize / 2 - 1; + final int dotY = (h - dotSize) / 2; + + if (commit.getChildCount() > 0) + drawLine(myColor, myLaneX, 0, myLaneX, dotY, LINE_WIDTH); + + if (commit.has(RevFlag.UNINTERESTING)) + drawBoundaryDot(dotX, dotY, dotSize, dotSize); + else + drawCommitDot(dotX, dotY, dotSize, dotSize); + + int textx = Math.max(maxCenter + LANE_WIDTH / 2, dotX + dotSize) + 8; + int n = commit.refs == null ? 0 : commit.refs.length; + for (int i = 0; i < n; ++i) { + textx += drawLabel(textx + dotSize, h/2, commit.refs[i]); + } + + final String msg = commit.getShortMessage(); + drawText(msg, textx + dotSize + n*2, h / 2); + } + + /** + * Draw a decoration for the Ref ref at x,y + * + * @param x + * left + * @param y + * top + * @param ref + * A peeled ref + * @return width of label in pixels + */ + protected abstract int drawLabel(int x, int y, Ref ref); + + private int computeDotSize(final int h) { + int d = (int) (Math.min(h, LANE_WIDTH) * 0.50f); + d += (d & 1); + return d; + } + + /** + * Obtain the color reference used to paint this lane. + * <p> + * Colors returned by this method will be passed to the other drawing + * primitives, so the color returned should be application specific. + * <p> + * If a null lane is supplied the return value must still be acceptable to a + * drawing method. Usually this means the implementation should return a + * default color. + * + * @param myLane + * the current lane. May be null. + * @return graphics specific color reference. Must be a valid color. + */ + protected abstract TColor laneColor(TLane myLane); + + /** + * Draw a single line within this cell. + * + * @param color + * the color to use while drawing the line. + * @param x1 + * starting X coordinate, 0 based. + * @param y1 + * starting Y coordinate, 0 based. + * @param x2 + * ending X coordinate, 0 based. + * @param y2 + * ending Y coordinate, 0 based. + * @param width + * number of pixels wide for the line. Always at least 1. + */ + protected abstract void drawLine(TColor color, int x1, int y1, int x2, + int y2, int width); + + /** + * Draw a single commit dot. + * <p> + * Usually the commit dot is a filled oval in blue, then a drawn oval in + * black, using the same coordinates for both operations. + * + * @param x + * upper left of the oval's bounding box. + * @param y + * upper left of the oval's bounding box. + * @param w + * width of the oval's bounding box. + * @param h + * height of the oval's bounding box. + */ + protected abstract void drawCommitDot(int x, int y, int w, int h); + + /** + * Draw a single boundary commit (aka uninteresting commit) dot. + * <p> + * Usually a boundary commit dot is a light gray oval with a white center. + * + * @param x + * upper left of the oval's bounding box. + * @param y + * upper left of the oval's bounding box. + * @param w + * width of the oval's bounding box. + * @param h + * height of the oval's bounding box. + */ + protected abstract void drawBoundaryDot(int x, int y, int w, int h); + + /** + * Draw a single line of text. + * <p> + * The font and colors used to render the text are left up to the + * implementation. + * + * @param msg + * the text to draw. Does not contain LFs. + * @param x + * first pixel from the left that the text can be drawn at. + * Character data must not appear before this position. + * @param y + * pixel coordinate of the centerline of the text. + * Implementations must adjust this coordinate to account for the + * way their implementation handles font rendering. + */ + protected abstract void drawText(String msg, int x, int y); + + private int laneX(final PlotLane myLane) { + final int p = myLane != null ? myLane.getPosition() : 0; + return LEFT_PAD + LANE_WIDTH * p; + } + + private int laneC(final PlotLane myLane) { + return laneX(myLane) + LANE_WIDTH / 2; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java new file mode 100644 index 0000000000..54d7c013d6 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revplot; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.lib.Ref; + +/** + * A commit reference to a commit in the DAG. + * + * @param <L> + * type of lane being used by the plotter. + * @see PlotCommitList + */ +public class PlotCommit<L extends PlotLane> extends RevCommit { + static final PlotCommit[] NO_CHILDREN = {}; + + static final PlotLane[] NO_LANES = {}; + + PlotLane[] passingLanes; + + PlotLane lane; + + PlotCommit[] children; + + final Ref[] refs; + + /** + * Create a new commit. + * + * @param id + * the identity of this commit. + * @param tags + * the tags associated with this commit, null for no tags + */ + protected PlotCommit(final AnyObjectId id, final Ref[] tags) { + super(id); + this.refs = tags; + passingLanes = NO_LANES; + children = NO_CHILDREN; + } + + void addPassingLane(final PlotLane c) { + final int cnt = passingLanes.length; + if (cnt == 0) + passingLanes = new PlotLane[] { c }; + else if (cnt == 1) + passingLanes = new PlotLane[] { passingLanes[0], c }; + else { + final PlotLane[] n = new PlotLane[cnt + 1]; + System.arraycopy(passingLanes, 0, n, 0, cnt); + n[cnt] = c; + passingLanes = n; + } + } + + void addChild(final PlotCommit c) { + final int cnt = children.length; + if (cnt == 0) + children = new PlotCommit[] { c }; + else if (cnt == 1) + children = new PlotCommit[] { children[0], c }; + else { + final PlotCommit[] n = new PlotCommit[cnt + 1]; + System.arraycopy(children, 0, n, 0, cnt); + n[cnt] = c; + children = n; + } + } + + /** + * Get the number of child commits listed in this commit. + * + * @return number of children; always a positive value but can be 0. + */ + public final int getChildCount() { + return children.length; + } + + /** + * Get the nth child from this commit's child list. + * + * @param nth + * child index to obtain. Must be in the range 0 through + * {@link #getChildCount()}-1. + * @return the specified child. + * @throws ArrayIndexOutOfBoundsException + * an invalid child index was specified. + */ + public final PlotCommit getChild(final int nth) { + return children[nth]; + } + + /** + * Determine if the given commit is a child (descendant) of this commit. + * + * @param c + * the commit to test. + * @return true if the given commit built on top of this commit. + */ + public final boolean isChild(final PlotCommit c) { + for (final PlotCommit a : children) + if (a == c) + return true; + return false; + } + + /** + * Obtain the lane this commit has been plotted into. + * + * @return the assigned lane for this commit. + */ + public final L getLane() { + return (L) lane; + } + + @Override + public void reset() { + passingLanes = NO_LANES; + children = NO_CHILDREN; + lane = null; + super.reset(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java new file mode 100644 index 0000000000..7c27a86f49 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revplot; + +import java.util.Collection; +import java.util.HashSet; +import java.util.TreeSet; + +import org.eclipse.jgit.revwalk.RevCommitList; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * An ordered list of {@link PlotCommit} subclasses. + * <p> + * Commits are allocated into lanes as they enter the list, based upon their + * connections between descendant (child) commits and ancestor (parent) commits. + * <p> + * The source of the list must be a {@link PlotWalk} and {@link #fillTo(int)} + * must be used to populate the list. + * + * @param <L> + * type of lane used by the application. + */ +public class PlotCommitList<L extends PlotLane> extends + RevCommitList<PlotCommit<L>> { + static final int MAX_LENGTH = 25; + + private int lanesAllocated; + + private final TreeSet<Integer> freeLanes = new TreeSet<Integer>(); + + private HashSet<PlotLane> activeLanes = new HashSet<PlotLane>(32); + + @Override + public void source(final RevWalk w) { + if (!(w instanceof PlotWalk)) + throw new ClassCastException("Not a " + PlotWalk.class.getName()); + super.source(w); + } + + /** + * Find the set of lanes passing through a commit's row. + * <p> + * Lanes passing through a commit are lanes that the commit is not directly + * on, but that need to travel through this commit to connect a descendant + * (child) commit to an ancestor (parent) commit. Typically these lanes will + * be drawn as lines in the passed commit's box, and the passed commit won't + * appear to be connected to those lines. + * <p> + * This method modifies the passed collection by adding the lanes in any + * order. + * + * @param currCommit + * the commit the caller needs to get the lanes from. + * @param result + * collection to add the passing lanes into. + */ + public void findPassingThrough(final PlotCommit<L> currCommit, + final Collection<L> result) { + for (final PlotLane p : currCommit.passingLanes) + result.add((L) p); + } + + @Override + protected void enter(final int index, final PlotCommit<L> currCommit) { + setupChildren(currCommit); + + final int nChildren = currCommit.getChildCount(); + if (nChildren == 0) + return; + + if (nChildren == 1 && currCommit.children[0].getParentCount() < 2) { + // Only one child, child has only us as their parent. + // Stay in the same lane as the child. + // + final PlotCommit c = currCommit.children[0]; + if (c.lane == null) { + // Hmmph. This child must be the first along this lane. + // + c.lane = nextFreeLane(); + activeLanes.add(c.lane); + } + + for (int r = index - 1; r >= 0; r--) { + final PlotCommit rObj = get(r); + if (rObj == c) + break; + rObj.addPassingLane(c.lane); + } + currCommit.lane = c.lane; + currCommit.lane.parent = currCommit; + } else { + // More than one child, or our child is a merge. + // Use a different lane. + // + + for (int i = 0; i < nChildren; i++) { + final PlotCommit c = currCommit.children[i]; + if (activeLanes.remove(c.lane)) { + recycleLane((L) c.lane); + freeLanes.add(Integer.valueOf(c.lane.position)); + } + } + + currCommit.lane = nextFreeLane(); + currCommit.lane.parent = currCommit; + activeLanes.add(currCommit.lane); + + int remaining = nChildren; + for (int r = index - 1; r >= 0; r--) { + final PlotCommit rObj = get(r); + if (currCommit.isChild(rObj)) { + if (--remaining == 0) + break; + } + rObj.addPassingLane(currCommit.lane); + } + } + } + + private void setupChildren(final PlotCommit<L> currCommit) { + final int nParents = currCommit.getParentCount(); + for (int i = 0; i < nParents; i++) + ((PlotCommit) currCommit.getParent(i)).addChild(currCommit); + } + + private PlotLane nextFreeLane() { + final PlotLane p = createLane(); + if (freeLanes.isEmpty()) { + p.position = lanesAllocated++; + } else { + final Integer min = freeLanes.first(); + p.position = min.intValue(); + freeLanes.remove(min); + } + return p; + } + + /** + * @return a new Lane appropriate for this particular PlotList. + */ + protected L createLane() { + return (L) new PlotLane(); + } + + /** + * Return colors and other reusable information to the plotter when a lane + * is no longer needed. + * + * @param lane + */ + protected void recycleLane(final L lane) { + // Nothing. + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotLane.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotLane.java new file mode 100644 index 0000000000..45dd9960df --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotLane.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revplot; + +/** + * A line space within the graph. + * <p> + * Commits are strung onto a lane. For many UIs a lane represents a column. + */ +public class PlotLane { + PlotCommit parent; + + int position; + + /** + * Logical location of this lane within the graphing plane. + * + * @return location of this lane, 0 through the maximum number of lanes. + */ + public int getPosition() { + return position; + } + + public int hashCode() { + return position; + } + + public boolean equals(final Object o) { + return o == this; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotWalk.java new file mode 100644 index 0000000000..bebe148ebb --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotWalk.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2008-2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revplot; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Commit; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Tag; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevSort; +import org.eclipse.jgit.revwalk.RevWalk; + +/** Specialized RevWalk for visualization of a commit graph. */ +public class PlotWalk extends RevWalk { + + private Map<AnyObjectId, Set<Ref>> reverseRefMap; + + @Override + public void dispose() { + super.dispose(); + reverseRefMap.clear(); + } + + /** + * Create a new revision walker for a given repository. + * + * @param repo + * the repository the walker will obtain data from. + */ + public PlotWalk(final Repository repo) { + super(repo); + super.sort(RevSort.TOPO, true); + reverseRefMap = repo.getAllRefsByPeeledObjectId(); + } + + @Override + public void sort(final RevSort s, final boolean use) { + if (s == RevSort.TOPO && !use) + throw new IllegalArgumentException("Topological sort required."); + super.sort(s, use); + } + + @Override + protected RevCommit createCommit(final AnyObjectId id) { + return new PlotCommit(id, getTags(id)); + } + + /** + * @param commitId + * @return return the list of knows tags referring to this commit + */ + protected Ref[] getTags(final AnyObjectId commitId) { + Collection<Ref> list = reverseRefMap.get(commitId); + Ref[] tags; + if (list == null) + tags = null; + else { + tags = list.toArray(new Ref[list.size()]); + Arrays.sort(tags, new PlotRefComparator()); + } + return tags; + } + + class PlotRefComparator implements Comparator<Ref> { + public int compare(Ref o1, Ref o2) { + try { + Object obj1 = getRepository().mapObject(o1.getObjectId(), o1.getName()); + Object obj2 = getRepository().mapObject(o2.getObjectId(), o2.getName()); + long t1 = timeof(obj1); + long t2 = timeof(obj2); + if (t1 > t2) + return -1; + if (t1 < t2) + return 1; + return 0; + } catch (IOException e) { + // ignore + return 0; + } + } + long timeof(Object o) { + if (o instanceof Commit) + return ((Commit)o).getCommitter().getWhen().getTime(); + if (o instanceof Tag) + return ((Tag)o).getTagger().getWhen().getTime(); + return 0; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AbstractRevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AbstractRevQueue.java new file mode 100644 index 0000000000..30d29a80b4 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AbstractRevQueue.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +abstract class AbstractRevQueue extends Generator { + static final AbstractRevQueue EMPTY_QUEUE = new AlwaysEmptyQueue(); + + /** Current output flags set for this generator instance. */ + int outputType; + + /** + * Add a commit to the queue. + * <p> + * This method always adds the commit, even if it is already in the queue or + * previously was in the queue but has already been removed. To control + * queue admission use {@link #add(RevCommit, RevFlag)}. + * + * @param c + * commit to add. + */ + public abstract void add(RevCommit c); + + /** + * Add a commit if it does not have a flag set yet, then set the flag. + * <p> + * This method permits the application to test if the commit has the given + * flag; if it does not already have the flag than the commit is added to + * the queue and the flag is set. This later will prevent the commit from + * being added twice. + * + * @param c + * commit to add. + * @param queueControl + * flag that controls admission to the queue. + */ + public final void add(final RevCommit c, final RevFlag queueControl) { + if (!c.has(queueControl)) { + c.add(queueControl); + add(c); + } + } + + /** + * Add a commit's parents if one does not have a flag set yet. + * <p> + * This method permits the application to test if the commit has the given + * flag; if it does not already have the flag than the commit is added to + * the queue and the flag is set. This later will prevent the commit from + * being added twice. + * + * @param c + * commit whose parents should be added. + * @param queueControl + * flag that controls admission to the queue. + */ + public final void addParents(final RevCommit c, final RevFlag queueControl) { + final RevCommit[] pList = c.parents; + if (pList == null) + return; + for (RevCommit p : pList) + add(p, queueControl); + } + + /** + * Remove the first commit from the queue. + * + * @return the first commit of this queue. + */ + public abstract RevCommit next(); + + /** Remove all entries from this queue. */ + public abstract void clear(); + + abstract boolean everbodyHasFlag(int f); + + abstract boolean anybodyHasFlag(int f); + + @Override + int outputType() { + return outputType; + } + + protected static void describe(final StringBuilder s, final RevCommit c) { + s.append(c.toString()); + s.append('\n'); + } + + private static class AlwaysEmptyQueue extends AbstractRevQueue { + @Override + public void add(RevCommit c) { + throw new UnsupportedOperationException(); + } + + @Override + public RevCommit next() { + return null; + } + + @Override + boolean anybodyHasFlag(int f) { + return false; + } + + @Override + boolean everbodyHasFlag(int f) { + return true; + } + + @Override + public void clear() { + // Nothing to clear, we have no state. + } + + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockObjQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockObjQueue.java new file mode 100644 index 0000000000..371cd06dda --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockObjQueue.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +class BlockObjQueue { + private BlockFreeList free; + + private Block head; + + private Block tail; + + /** Create an empty queue. */ + BlockObjQueue() { + free = new BlockFreeList(); + } + + void add(final RevObject c) { + Block b = tail; + if (b == null) { + b = free.newBlock(); + b.add(c); + head = b; + tail = b; + return; + } else if (b.isFull()) { + b = free.newBlock(); + tail.next = b; + tail = b; + } + b.add(c); + } + + RevObject next() { + final Block b = head; + if (b == null) + return null; + + final RevObject c = b.pop(); + if (b.isEmpty()) { + head = b.next; + if (head == null) + tail = null; + free.freeBlock(b); + } + return c; + } + + static final class BlockFreeList { + private Block next; + + Block newBlock() { + Block b = next; + if (b == null) + return new Block(); + next = b.next; + b.clear(); + return b; + } + + void freeBlock(final Block b) { + b.next = next; + next = b; + } + } + + static final class Block { + private static final int BLOCK_SIZE = 256; + + /** Next block in our chain of blocks; null if we are the last. */ + Block next; + + /** Our table of queued objects. */ + final RevObject[] objects = new RevObject[BLOCK_SIZE]; + + /** Next valid entry in {@link #objects}. */ + int headIndex; + + /** Next free entry in {@link #objects} for addition at. */ + int tailIndex; + + boolean isFull() { + return tailIndex == BLOCK_SIZE; + } + + boolean isEmpty() { + return headIndex == tailIndex; + } + + void add(final RevObject c) { + objects[tailIndex++] = c; + } + + RevObject pop() { + return objects[headIndex++]; + } + + void clear() { + next = null; + headIndex = 0; + tailIndex = 0; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockRevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockRevQueue.java new file mode 100644 index 0000000000..5e7a7998e0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockRevQueue.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; + +abstract class BlockRevQueue extends AbstractRevQueue { + protected BlockFreeList free; + + /** Create an empty revision queue. */ + protected BlockRevQueue() { + free = new BlockFreeList(); + } + + BlockRevQueue(final Generator s) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + free = new BlockFreeList(); + outputType = s.outputType(); + s.shareFreeList(this); + for (;;) { + final RevCommit c = s.next(); + if (c == null) + break; + add(c); + } + } + + /** + * Reconfigure this queue to share the same free list as another. + * <p> + * Multiple revision queues can be connected to the same free list, making + * it less expensive for applications to shuttle commits between them. This + * method arranges for the receiver to take from / return to the same free + * list as the supplied queue. + * <p> + * Free lists are not thread-safe. Applications must ensure that all queues + * sharing the same free list are doing so from only a single thread. + * + * @param q + * the other queue we will steal entries from. + */ + public void shareFreeList(final BlockRevQueue q) { + free = q.free; + } + + static final class BlockFreeList { + private Block next; + + Block newBlock() { + Block b = next; + if (b == null) + return new Block(); + next = b.next; + b.clear(); + return b; + } + + void freeBlock(final Block b) { + b.next = next; + next = b; + } + + void clear() { + next = null; + } + } + + static final class Block { + static final int BLOCK_SIZE = 256; + + /** Next block in our chain of blocks; null if we are the last. */ + Block next; + + /** Our table of queued commits. */ + final RevCommit[] commits = new RevCommit[BLOCK_SIZE]; + + /** Next valid entry in {@link #commits}. */ + int headIndex; + + /** Next free entry in {@link #commits} for addition at. */ + int tailIndex; + + boolean isFull() { + return tailIndex == BLOCK_SIZE; + } + + boolean isEmpty() { + return headIndex == tailIndex; + } + + boolean canUnpop() { + return headIndex > 0; + } + + void add(final RevCommit c) { + commits[tailIndex++] = c; + } + + void unpop(final RevCommit c) { + commits[--headIndex] = c; + } + + RevCommit pop() { + return commits[headIndex++]; + } + + RevCommit peek() { + return commits[headIndex]; + } + + void clear() { + next = null; + headIndex = 0; + tailIndex = 0; + } + + void resetToMiddle() { + headIndex = tailIndex = BLOCK_SIZE / 2; + } + + void resetToEnd() { + headIndex = tailIndex = BLOCK_SIZE; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BoundaryGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BoundaryGenerator.java new file mode 100644 index 0000000000..6be0c8584e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BoundaryGenerator.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; + +class BoundaryGenerator extends Generator { + static final int UNINTERESTING = RevWalk.UNINTERESTING; + + Generator g; + + BoundaryGenerator(final RevWalk w, final Generator s) { + g = new InitialGenerator(w, s); + } + + @Override + int outputType() { + return g.outputType() | HAS_UNINTERESTING; + } + + @Override + void shareFreeList(final BlockRevQueue q) { + g.shareFreeList(q); + } + + @Override + RevCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + return g.next(); + } + + private class InitialGenerator extends Generator { + private static final int PARSED = RevWalk.PARSED; + + private static final int DUPLICATE = RevWalk.TEMP_MARK; + + private final RevWalk walk; + + private final FIFORevQueue held; + + private final Generator source; + + InitialGenerator(final RevWalk w, final Generator s) { + walk = w; + held = new FIFORevQueue(); + source = s; + source.shareFreeList(held); + } + + @Override + int outputType() { + return source.outputType(); + } + + @Override + void shareFreeList(final BlockRevQueue q) { + q.shareFreeList(held); + } + + @Override + RevCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + RevCommit c = source.next(); + if (c != null) { + for (final RevCommit p : c.parents) + if ((p.flags & UNINTERESTING) != 0) + held.add(p); + return c; + } + + final FIFORevQueue boundary = new FIFORevQueue(); + boundary.shareFreeList(held); + for (;;) { + c = held.next(); + if (c == null) + break; + if ((c.flags & DUPLICATE) != 0) + continue; + if ((c.flags & PARSED) == 0) + c.parseHeaders(walk); + c.flags |= DUPLICATE; + boundary.add(c); + } + boundary.removeFlag(DUPLICATE); + g = boundary; + return boundary.next(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java new file mode 100644 index 0000000000..6ce63fe168 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; + +/** A queue of commits sorted by commit time order. */ +public class DateRevQueue extends AbstractRevQueue { + private Entry head; + + private Entry free; + + /** Create an empty date queue. */ + public DateRevQueue() { + super(); + } + + DateRevQueue(final Generator s) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + for (;;) { + final RevCommit c = s.next(); + if (c == null) + break; + add(c); + } + } + + public void add(final RevCommit c) { + Entry q = head; + final long when = c.commitTime; + final Entry n = newEntry(c); + if (q == null || when > q.commit.commitTime) { + n.next = q; + head = n; + } else { + Entry p = q.next; + while (p != null && p.commit.commitTime > when) { + q = p; + p = q.next; + } + n.next = q.next; + q.next = n; + } + } + + public RevCommit next() { + final Entry q = head; + if (q == null) + return null; + head = q.next; + freeEntry(q); + return q.commit; + } + + /** + * Peek at the next commit, without removing it. + * + * @return the next available commit; null if there are no commits left. + */ + public RevCommit peek() { + return head != null ? head.commit : null; + } + + public void clear() { + head = null; + free = null; + } + + boolean everbodyHasFlag(final int f) { + for (Entry q = head; q != null; q = q.next) { + if ((q.commit.flags & f) == 0) + return false; + } + return true; + } + + boolean anybodyHasFlag(final int f) { + for (Entry q = head; q != null; q = q.next) { + if ((q.commit.flags & f) != 0) + return true; + } + return false; + } + + @Override + int outputType() { + return outputType | SORT_COMMIT_TIME_DESC; + } + + public String toString() { + final StringBuilder s = new StringBuilder(); + for (Entry q = head; q != null; q = q.next) + describe(s, q.commit); + return s.toString(); + } + + private Entry newEntry(final RevCommit c) { + Entry r = free; + if (r == null) + r = new Entry(); + else + free = r.next; + r.commit = c; + return r; + } + + private void freeEntry(final Entry e) { + e.next = free; + free = e; + } + + static class Entry { + Entry next; + + RevCommit commit; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DelayRevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DelayRevQueue.java new file mode 100644 index 0000000000..4a0d19d60d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DelayRevQueue.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; + +/** + * Delays commits to be at least {@link PendingGenerator#OVER_SCAN} late. + * <p> + * This helps to "fix up" weird corner cases resulting from clock skew, by + * slowing down what we produce to the caller we get a better chance to ensure + * PendingGenerator reached back far enough in the graph to correctly mark + * commits {@link RevWalk#UNINTERESTING} if necessary. + * <p> + * This generator should appear before {@link FixUninterestingGenerator} if the + * lower level {@link #pending} isn't already fully buffered. + */ +final class DelayRevQueue extends Generator { + private static final int OVER_SCAN = PendingGenerator.OVER_SCAN; + + private final Generator pending; + + private final FIFORevQueue delay; + + private int size; + + DelayRevQueue(final Generator g) { + pending = g; + delay = new FIFORevQueue(); + } + + @Override + int outputType() { + return pending.outputType(); + } + + @Override + RevCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + while (size < OVER_SCAN) { + final RevCommit c = pending.next(); + if (c == null) + break; + delay.add(c); + size++; + } + + final RevCommit c = delay.next(); + if (c == null) + return null; + size--; + return c; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/EndGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/EndGenerator.java new file mode 100644 index 0000000000..627e1c7a51 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/EndGenerator.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +class EndGenerator extends Generator { + static final EndGenerator INSTANCE = new EndGenerator(); + + private EndGenerator() { + // We have nothing to initialize. + } + + @Override + RevCommit next() { + return null; + } + + @Override + int outputType() { + return 0; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FIFORevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FIFORevQueue.java new file mode 100644 index 0000000000..5690a5d869 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FIFORevQueue.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; + +/** A queue of commits in FIFO order. */ +public class FIFORevQueue extends BlockRevQueue { + private Block head; + + private Block tail; + + /** Create an empty FIFO queue. */ + public FIFORevQueue() { + super(); + } + + FIFORevQueue(final Generator s) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + super(s); + } + + public void add(final RevCommit c) { + Block b = tail; + if (b == null) { + b = free.newBlock(); + b.add(c); + head = b; + tail = b; + return; + } else if (b.isFull()) { + b = free.newBlock(); + tail.next = b; + tail = b; + } + b.add(c); + } + + /** + * Insert the commit pointer at the front of the queue. + * + * @param c + * the commit to insert into the queue. + */ + public void unpop(final RevCommit c) { + Block b = head; + if (b == null) { + b = free.newBlock(); + b.resetToMiddle(); + b.add(c); + head = b; + tail = b; + return; + } else if (b.canUnpop()) { + b.unpop(c); + return; + } + + b = free.newBlock(); + b.resetToEnd(); + b.unpop(c); + b.next = head; + head = b; + } + + public RevCommit next() { + final Block b = head; + if (b == null) + return null; + + final RevCommit c = b.pop(); + if (b.isEmpty()) { + head = b.next; + if (head == null) + tail = null; + free.freeBlock(b); + } + return c; + } + + public void clear() { + head = null; + tail = null; + free.clear(); + } + + boolean everbodyHasFlag(final int f) { + for (Block b = head; b != null; b = b.next) { + for (int i = b.headIndex; i < b.tailIndex; i++) + if ((b.commits[i].flags & f) == 0) + return false; + } + return true; + } + + boolean anybodyHasFlag(final int f) { + for (Block b = head; b != null; b = b.next) { + for (int i = b.headIndex; i < b.tailIndex; i++) + if ((b.commits[i].flags & f) != 0) + return true; + } + return false; + } + + void removeFlag(final int f) { + final int not_f = ~f; + for (Block b = head; b != null; b = b.next) { + for (int i = b.headIndex; i < b.tailIndex; i++) + b.commits[i].flags &= not_f; + } + } + + public String toString() { + final StringBuilder s = new StringBuilder(); + for (Block q = head; q != null; q = q.next) { + for (int i = q.headIndex; i < q.tailIndex; i++) + describe(s, q.commits[i]); + } + return s.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FixUninterestingGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FixUninterestingGenerator.java new file mode 100644 index 0000000000..9d734a7296 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FixUninterestingGenerator.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; + +/** + * Filters out commits marked {@link RevWalk#UNINTERESTING}. + * <p> + * This generator is only in front of another generator that has fully buffered + * commits, such that we are called only after the {@link PendingGenerator} has + * exhausted its input queue and given up. It skips over any uninteresting + * commits that may have leaked out of the PendingGenerator due to clock skew + * being detected in the commit objects. + */ +final class FixUninterestingGenerator extends Generator { + private final Generator pending; + + FixUninterestingGenerator(final Generator g) { + pending = g; + } + + @Override + int outputType() { + return pending.outputType(); + } + + @Override + RevCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + for (;;) { + final RevCommit c = pending.next(); + if (c == null) + return null; + if ((c.flags & RevWalk.UNINTERESTING) == 0) + return c; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterKey.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterKey.java new file mode 100644 index 0000000000..97a8ab2ad2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterKey.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import org.eclipse.jgit.lib.Constants; + +/** Case insensitive key for a {@link FooterLine}. */ +public final class FooterKey { + /** Standard {@code Signed-off-by} */ + public static final FooterKey SIGNED_OFF_BY = new FooterKey("Signed-off-by"); + + /** Standard {@code Acked-by} */ + public static final FooterKey ACKED_BY = new FooterKey("Acked-by"); + + /** Standard {@code CC} */ + public static final FooterKey CC = new FooterKey("CC"); + + private final String name; + + final byte[] raw; + + /** + * Create a key for a specific footer line. + * + * @param keyName + * name of the footer line. + */ + public FooterKey(final String keyName) { + name = keyName; + raw = Constants.encode(keyName.toLowerCase()); + } + + /** @return name of this footer line. */ + public String getName() { + return name; + } + + @Override + public String toString() { + return "FooterKey[" + name + "]"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java new file mode 100644 index 0000000000..541f2748e7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FooterLine.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.nio.charset.Charset; + +import org.eclipse.jgit.util.RawParseUtils; + +/** + * Single line at the end of a message, such as a "Signed-off-by: someone". + * <p> + * These footer lines tend to be used to represent additional information about + * a commit, like the path it followed through reviewers before finally being + * accepted into the project's main repository as an immutable commit. + * + * @see RevCommit#getFooterLines() + */ +public final class FooterLine { + private final byte[] buffer; + + private final Charset enc; + + private final int keyStart; + + private final int keyEnd; + + private final int valStart; + + private final int valEnd; + + FooterLine(final byte[] b, final Charset e, final int ks, final int ke, + final int vs, final int ve) { + buffer = b; + enc = e; + keyStart = ks; + keyEnd = ke; + valStart = vs; + valEnd = ve; + } + + /** + * @param key + * key to test this line's key name against. + * @return true if {@code key.getName().equalsIgnorecase(getKey())}. + */ + public boolean matches(final FooterKey key) { + final byte[] kRaw = key.raw; + final int len = kRaw.length; + int bPtr = keyStart; + if (keyEnd - bPtr != len) + return false; + for (int kPtr = 0; bPtr < len;) { + byte b = buffer[bPtr++]; + if ('A' <= b && b <= 'Z') + b += 'a' - 'A'; + if (b != kRaw[kPtr++]) + return false; + } + return true; + } + + /** + * @return key name of this footer; that is the text before the ":" on the + * line footer's line. The text is decoded according to the commit's + * specified (or assumed) character encoding. + */ + public String getKey() { + return RawParseUtils.decode(enc, buffer, keyStart, keyEnd); + } + + /** + * @return value of this footer; that is the text after the ":" and any + * leading whitespace has been skipped. May be the empty string if + * the footer has no value (line ended with ":"). The text is + * decoded according to the commit's specified (or assumed) + * character encoding. + */ + public String getValue() { + return RawParseUtils.decode(enc, buffer, valStart, valEnd); + } + + /** + * Extract the email address (if present) from the footer. + * <p> + * If there is an email address looking string inside of angle brackets + * (e.g. "<a@b>"), the return value is the part extracted from inside the + * brackets. If no brackets are found, then {@link #getValue()} is returned + * if the value contains an '@' sign. Otherwise, null. + * + * @return email address appearing in the value of this footer, or null. + */ + public String getEmailAddress() { + final int lt = RawParseUtils.nextLF(buffer, valStart, '<'); + if (valEnd <= lt) { + final int at = RawParseUtils.nextLF(buffer, valStart, '@'); + if (valStart < at && at < valEnd) + return getValue(); + return null; + } + final int gt = RawParseUtils.nextLF(buffer, lt, '>'); + if (valEnd < gt) + return null; + return RawParseUtils.decode(enc, buffer, lt, gt - 1); + } + + @Override + public String toString() { + return getKey() + ": " + getValue(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/Generator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/Generator.java new file mode 100644 index 0000000000..de9fabc196 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/Generator.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; + +/** + * Produces commits for RevWalk to return to applications. + * <p> + * Implementations of this basic class provide the real work behind RevWalk. + * Conceptually a Generator is an iterator or a queue, it returns commits until + * there are no more relevant. Generators may be piped/stacked together to + * create a more complex set of operations. + * + * @see PendingGenerator + * @see StartGenerator + */ +abstract class Generator { + /** Commits are sorted by commit date and time, descending. */ + static final int SORT_COMMIT_TIME_DESC = 1 << 0; + + /** Output may have {@link RevWalk#REWRITE} marked on it. */ + static final int HAS_REWRITE = 1 << 1; + + /** Output needs {@link RewriteGenerator}. */ + static final int NEEDS_REWRITE = 1 << 2; + + /** Topological ordering is enforced (all children before parents). */ + static final int SORT_TOPO = 1 << 3; + + /** Output may have {@link RevWalk#UNINTERESTING} marked on it. */ + static final int HAS_UNINTERESTING = 1 << 4; + + /** + * Connect the supplied queue to this generator's own free list (if any). + * + * @param q + * another FIFO queue that wants to share our queue's free list. + */ + void shareFreeList(final BlockRevQueue q) { + // Do nothing by default. + } + + /** + * Obtain flags describing the output behavior of this generator. + * + * @return one or more of the constants declared in this class, describing + * how this generator produces its results. + */ + abstract int outputType(); + + /** + * Return the next commit to the application, or the next generator. + * + * @return next available commit; null if no more are to be returned. + * @throws MissingObjectException + * @throws IncorrectObjectTypeException + * @throws IOException + */ + abstract RevCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/LIFORevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/LIFORevQueue.java new file mode 100644 index 0000000000..9abaf8dccf --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/LIFORevQueue.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; + +/** A queue of commits in LIFO order. */ +public class LIFORevQueue extends BlockRevQueue { + private Block head; + + /** Create an empty LIFO queue. */ + public LIFORevQueue() { + super(); + } + + LIFORevQueue(final Generator s) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + super(s); + } + + public void add(final RevCommit c) { + Block b = head; + if (b == null || !b.canUnpop()) { + b = free.newBlock(); + b.resetToEnd(); + b.next = head; + head = b; + } + b.unpop(c); + } + + public RevCommit next() { + final Block b = head; + if (b == null) + return null; + + final RevCommit c = b.pop(); + if (b.isEmpty()) { + head = b.next; + free.freeBlock(b); + } + return c; + } + + public void clear() { + head = null; + free.clear(); + } + + boolean everbodyHasFlag(final int f) { + for (Block b = head; b != null; b = b.next) { + for (int i = b.headIndex; i < b.tailIndex; i++) + if ((b.commits[i].flags & f) == 0) + return false; + } + return true; + } + + boolean anybodyHasFlag(final int f) { + for (Block b = head; b != null; b = b.next) { + for (int i = b.headIndex; i < b.tailIndex; i++) + if ((b.commits[i].flags & f) != 0) + return true; + } + return false; + } + + public String toString() { + final StringBuilder s = new StringBuilder(); + for (Block q = head; q != null; q = q.next) { + for (int i = q.headIndex; i < q.tailIndex; i++) + describe(s, q.commits[i]); + } + return s.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java new file mode 100644 index 0000000000..2f01f541de --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; + +/** + * Computes the merge base(s) of the starting commits. + * <p> + * This generator is selected if the RevFilter is only + * {@link org.eclipse.jgit.revwalk.filter.RevFilter#MERGE_BASE}. + * <p> + * To compute the merge base we assign a temporary flag to each of the starting + * commits. The maximum number of starting commits is bounded by the number of + * free flags available in the RevWalk when the generator is initialized. These + * flags will be automatically released on the next reset of the RevWalk, but + * not until then, as they are assigned to commits throughout the history. + * <p> + * Several internal flags are reused here for a different purpose, but this + * should not have any impact as this generator should be run alone, and without + * any other generators wrapped around it. + */ +class MergeBaseGenerator extends Generator { + private static final int PARSED = RevWalk.PARSED; + + private static final int IN_PENDING = RevWalk.SEEN; + + private static final int POPPED = RevWalk.TEMP_MARK; + + private static final int MERGE_BASE = RevWalk.REWRITE; + + private final RevWalk walker; + + private final DateRevQueue pending; + + private int branchMask; + + private int recarryTest; + + private int recarryMask; + + MergeBaseGenerator(final RevWalk w) { + walker = w; + pending = new DateRevQueue(); + } + + void init(final AbstractRevQueue p) { + try { + for (;;) { + final RevCommit c = p.next(); + if (c == null) + break; + add(c); + } + } finally { + // Always free the flags immediately. This ensures the flags + // will be available for reuse when the walk resets. + // + walker.freeFlag(branchMask); + + // Setup the condition used by carryOntoOne to detect a late + // merge base and produce it on the next round. + // + recarryTest = branchMask | POPPED; + recarryMask = branchMask | POPPED | MERGE_BASE; + } + } + + private void add(final RevCommit c) { + final int flag = walker.allocFlag(); + branchMask |= flag; + if ((c.flags & branchMask) != 0) { + // This should never happen. RevWalk ensures we get a + // commit admitted to the initial queue only once. If + // we see this marks aren't correctly erased. + // + throw new IllegalStateException("Stale RevFlags on " + c.name()); + } + c.flags |= flag; + pending.add(c); + } + + @Override + int outputType() { + return 0; + } + + @Override + RevCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + for (;;) { + final RevCommit c = pending.next(); + if (c == null) { + walker.curs.release(); + return null; + } + + for (final RevCommit p : c.parents) { + if ((p.flags & IN_PENDING) != 0) + continue; + if ((p.flags & PARSED) == 0) + p.parseHeaders(walker); + p.flags |= IN_PENDING; + pending.add(p); + } + + int carry = c.flags & branchMask; + boolean mb = carry == branchMask; + if (mb) { + // If we are a merge base make sure our ancestors are + // also flagged as being popped, so that they do not + // generate to the caller. + // + carry |= MERGE_BASE; + } + carryOntoHistory(c, carry); + + if ((c.flags & MERGE_BASE) != 0) { + // This commit is an ancestor of a merge base we already + // popped back to the caller. If everyone in pending is + // that way we are done traversing; if not we just need + // to move to the next available commit and try again. + // + if (pending.everbodyHasFlag(MERGE_BASE)) + return null; + continue; + } + c.flags |= POPPED; + + if (mb) { + c.flags |= MERGE_BASE; + return c; + } + } + } + + private void carryOntoHistory(RevCommit c, final int carry) { + for (;;) { + final RevCommit[] pList = c.parents; + if (pList == null) + return; + final int n = pList.length; + if (n == 0) + return; + + for (int i = 1; i < n; i++) { + final RevCommit p = pList[i]; + if (!carryOntoOne(p, carry)) + carryOntoHistory(p, carry); + } + + c = pList[0]; + if (carryOntoOne(c, carry)) + break; + } + } + + private boolean carryOntoOne(final RevCommit p, final int carry) { + final boolean haveAll = (p.flags & carry) == carry; + p.flags |= carry; + + if ((p.flags & recarryMask) == recarryTest) { + // We were popped without being a merge base, but we just got + // voted to be one. Inject ourselves back at the front of the + // pending queue and tell all of our ancestors they are within + // the merge base now. + // + p.flags &= ~POPPED; + pending.add(p); + carryOntoHistory(p, branchMask | MERGE_BASE); + return true; + } + + // If we already had all carried flags, our parents do too. + // Return true to stop the caller from running down this leg + // of the revision graph any further. + // + return haveAll; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java new file mode 100644 index 0000000000..d8f88ea305 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; + +/** + * Specialized subclass of RevWalk to include trees, blobs and tags. + * <p> + * Unlike RevWalk this subclass is able to remember starting roots that include + * annotated tags, or arbitrary trees or blobs. Once commit generation is + * complete and all commits have been popped by the application, individual + * annotated tag, tree and blob objects can be popped through the additional + * method {@link #nextObject()}. + * <p> + * Tree and blob objects reachable from interesting commits are automatically + * scheduled for inclusion in the results of {@link #nextObject()}, returning + * each object exactly once. Objects are sorted and returned according to the + * the commits that reference them and the order they appear within a tree. + * Ordering can be affected by changing the {@link RevSort} used to order the + * commits that are returned first. + */ +public class ObjectWalk extends RevWalk { + /** + * Indicates a non-RevCommit is in {@link #pendingObjects}. + * <p> + * We can safely reuse {@link RevWalk#REWRITE} here for the same value as it + * is only set on RevCommit and {@link #pendingObjects} never has RevCommit + * instances inserted into it. + */ + private static final int IN_PENDING = RevWalk.REWRITE; + + private CanonicalTreeParser treeWalk; + + private BlockObjQueue pendingObjects; + + private RevTree currentTree; + + private boolean fromTreeWalk; + + private RevTree nextSubtree; + + /** + * Create a new revision and object walker for a given repository. + * + * @param repo + * the repository the walker will obtain data from. + */ + public ObjectWalk(final Repository repo) { + super(repo); + pendingObjects = new BlockObjQueue(); + treeWalk = new CanonicalTreeParser(); + } + + /** + * Mark an object or commit to start graph traversal from. + * <p> + * Callers are encouraged to use {@link RevWalk#parseAny(AnyObjectId)} + * instead of {@link RevWalk#lookupAny(AnyObjectId, int)}, as this method + * requires the object to be parsed before it can be added as a root for the + * traversal. + * <p> + * The method will automatically parse an unparsed object, but error + * handling may be more difficult for the application to explain why a + * RevObject is not actually valid. The object pool of this walker would + * also be 'poisoned' by the invalid RevObject. + * <p> + * This method will automatically call {@link RevWalk#markStart(RevCommit)} + * if passed RevCommit instance, or a RevTag that directly (or indirectly) + * references a RevCommit. + * + * @param o + * the object to start traversing from. The object passed must be + * from this same revision walker. + * @throws MissingObjectException + * the object supplied is not available from the object + * database. This usually indicates the supplied object is + * invalid, but the reference was constructed during an earlier + * invocation to {@link RevWalk#lookupAny(AnyObjectId, int)}. + * @throws IncorrectObjectTypeException + * the object was not parsed yet and it was discovered during + * parsing that it is not actually the type of the instance + * passed in. This usually indicates the caller used the wrong + * type in a {@link RevWalk#lookupAny(AnyObjectId, int)} call. + * @throws IOException + * a pack file or loose object could not be read. + */ + public void markStart(RevObject o) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + while (o instanceof RevTag) { + addObject(o); + o = ((RevTag) o).getObject(); + parseHeaders(o); + } + + if (o instanceof RevCommit) + super.markStart((RevCommit) o); + else + addObject(o); + } + + /** + * Mark an object to not produce in the output. + * <p> + * Uninteresting objects denote not just themselves but also their entire + * reachable chain, back until the merge base of an uninteresting commit and + * an otherwise interesting commit. + * <p> + * Callers are encouraged to use {@link RevWalk#parseAny(AnyObjectId)} + * instead of {@link RevWalk#lookupAny(AnyObjectId, int)}, as this method + * requires the object to be parsed before it can be added as a root for the + * traversal. + * <p> + * The method will automatically parse an unparsed object, but error + * handling may be more difficult for the application to explain why a + * RevObject is not actually valid. The object pool of this walker would + * also be 'poisoned' by the invalid RevObject. + * <p> + * This method will automatically call {@link RevWalk#markStart(RevCommit)} + * if passed RevCommit instance, or a RevTag that directly (or indirectly) + * references a RevCommit. + * + * @param o + * the object to start traversing from. The object passed must be + * @throws MissingObjectException + * the object supplied is not available from the object + * database. This usually indicates the supplied object is + * invalid, but the reference was constructed during an earlier + * invocation to {@link RevWalk#lookupAny(AnyObjectId, int)}. + * @throws IncorrectObjectTypeException + * the object was not parsed yet and it was discovered during + * parsing that it is not actually the type of the instance + * passed in. This usually indicates the caller used the wrong + * type in a {@link RevWalk#lookupAny(AnyObjectId, int)} call. + * @throws IOException + * a pack file or loose object could not be read. + */ + public void markUninteresting(RevObject o) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + while (o instanceof RevTag) { + o.flags |= UNINTERESTING; + if (hasRevSort(RevSort.BOUNDARY)) + addObject(o); + o = ((RevTag) o).getObject(); + parseHeaders(o); + } + + if (o instanceof RevCommit) + super.markUninteresting((RevCommit) o); + else if (o instanceof RevTree) + markTreeUninteresting((RevTree) o); + else + o.flags |= UNINTERESTING; + + if (o.getType() != Constants.OBJ_COMMIT && hasRevSort(RevSort.BOUNDARY)) { + addObject(o); + } + } + + @Override + public RevCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + for (;;) { + final RevCommit r = super.next(); + if (r == null) + return null; + if ((r.flags & UNINTERESTING) != 0) { + markTreeUninteresting(r.getTree()); + if (hasRevSort(RevSort.BOUNDARY)) { + pendingObjects.add(r.getTree()); + return r; + } + continue; + } + pendingObjects.add(r.getTree()); + return r; + } + } + + /** + * Pop the next most recent object. + * + * @return next most recent object; null if traversal is over. + * @throws MissingObjectException + * one or or more of the next objects are not available from the + * object database, but were thought to be candidates for + * traversal. This usually indicates a broken link. + * @throws IncorrectObjectTypeException + * one or or more of the objects in a tree do not match the type + * indicated. + * @throws IOException + * a pack file or loose object could not be read. + */ + public RevObject nextObject() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + fromTreeWalk = false; + + if (nextSubtree != null) { + treeWalk = treeWalk.createSubtreeIterator0(db, nextSubtree, curs); + nextSubtree = null; + } + + while (!treeWalk.eof()) { + final FileMode mode = treeWalk.getEntryFileMode(); + final int sType = mode.getObjectType(); + + switch (sType) { + case Constants.OBJ_BLOB: { + treeWalk.getEntryObjectId(idBuffer); + final RevBlob o = lookupBlob(idBuffer); + if ((o.flags & SEEN) != 0) + break; + o.flags |= SEEN; + if (shouldSkipObject(o)) + break; + fromTreeWalk = true; + return o; + } + case Constants.OBJ_TREE: { + treeWalk.getEntryObjectId(idBuffer); + final RevTree o = lookupTree(idBuffer); + if ((o.flags & SEEN) != 0) + break; + o.flags |= SEEN; + if (shouldSkipObject(o)) + break; + nextSubtree = o; + fromTreeWalk = true; + return o; + } + default: + if (FileMode.GITLINK.equals(mode)) + break; + treeWalk.getEntryObjectId(idBuffer); + throw new CorruptObjectException("Invalid mode " + mode + + " for " + idBuffer.name() + " " + + treeWalk.getEntryPathString() + " in " + currentTree + + "."); + } + + treeWalk = treeWalk.next(); + } + + for (;;) { + final RevObject o = pendingObjects.next(); + if (o == null) + return null; + if ((o.flags & SEEN) != 0) + continue; + o.flags |= SEEN; + if (shouldSkipObject(o)) + continue; + if (o instanceof RevTree) { + currentTree = (RevTree) o; + treeWalk = treeWalk.resetRoot(db, currentTree, curs); + } + return o; + } + } + + private final boolean shouldSkipObject(final RevObject o) { + return (o.flags & UNINTERESTING) != 0 && !hasRevSort(RevSort.BOUNDARY); + } + + /** + * Verify all interesting objects are available, and reachable. + * <p> + * Callers should populate starting points and ending points with + * {@link #markStart(RevObject)} and {@link #markUninteresting(RevObject)} + * and then use this method to verify all objects between those two points + * exist in the repository and are readable. + * <p> + * This method returns successfully if everything is connected; it throws an + * exception if there is a connectivity problem. The exception message + * provides some detail about the connectivity failure. + * + * @throws MissingObjectException + * one or or more of the next objects are not available from the + * object database, but were thought to be candidates for + * traversal. This usually indicates a broken link. + * @throws IncorrectObjectTypeException + * one or or more of the objects in a tree do not match the type + * indicated. + * @throws IOException + * a pack file or loose object could not be read. + */ + public void checkConnectivity() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + for (;;) { + final RevCommit c = next(); + if (c == null) + break; + } + for (;;) { + final RevObject o = nextObject(); + if (o == null) + break; + if (o instanceof RevBlob && !db.hasObject(o)) + throw new MissingObjectException(o, Constants.TYPE_BLOB); + } + } + + /** + * Get the current object's complete path. + * <p> + * This method is not very efficient and is primarily meant for debugging + * and final output generation. Applications should try to avoid calling it, + * and if invoked do so only once per interesting entry, where the name is + * absolutely required for correct function. + * + * @return complete path of the current entry, from the root of the + * repository. If the current entry is in a subtree there will be at + * least one '/' in the returned string. Null if the current entry + * has no path, such as for annotated tags or root level trees. + */ + public String getPathString() { + return fromTreeWalk ? treeWalk.getEntryPathString() : null; + } + + @Override + public void dispose() { + super.dispose(); + pendingObjects = new BlockObjQueue(); + nextSubtree = null; + currentTree = null; + } + + @Override + protected void reset(final int retainFlags) { + super.reset(retainFlags); + pendingObjects = new BlockObjQueue(); + nextSubtree = null; + } + + private void addObject(final RevObject o) { + if ((o.flags & IN_PENDING) == 0) { + o.flags |= IN_PENDING; + pendingObjects.add(o); + } + } + + private void markTreeUninteresting(final RevTree tree) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + if ((tree.flags & UNINTERESTING) != 0) + return; + tree.flags |= UNINTERESTING; + + treeWalk = treeWalk.resetRoot(db, tree, curs); + while (!treeWalk.eof()) { + final FileMode mode = treeWalk.getEntryFileMode(); + final int sType = mode.getObjectType(); + + switch (sType) { + case Constants.OBJ_BLOB: { + treeWalk.getEntryObjectId(idBuffer); + lookupBlob(idBuffer).flags |= UNINTERESTING; + break; + } + case Constants.OBJ_TREE: { + treeWalk.getEntryObjectId(idBuffer); + final RevTree t = lookupTree(idBuffer); + if ((t.flags & UNINTERESTING) == 0) { + t.flags |= UNINTERESTING; + treeWalk = treeWalk.createSubtreeIterator0(db, t, curs); + continue; + } + break; + } + default: + if (FileMode.GITLINK.equals(mode)) + break; + treeWalk.getEntryObjectId(idBuffer); + throw new CorruptObjectException("Invalid mode " + mode + + " for " + idBuffer.name() + " " + + treeWalk.getEntryPathString() + " in " + tree + "."); + } + + treeWalk = treeWalk.next(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PendingGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PendingGenerator.java new file mode 100644 index 0000000000..e723bce51b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PendingGenerator.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.StopWalkException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.filter.RevFilter; + +/** + * Default (and first pass) RevCommit Generator implementation for RevWalk. + * <p> + * This generator starts from a set of one or more commits and process them in + * descending (newest to oldest) commit time order. Commits automatically cause + * their parents to be enqueued for further processing, allowing the entire + * commit graph to be walked. A {@link RevFilter} may be used to select a subset + * of the commits and return them to the caller. + */ +class PendingGenerator extends Generator { + private static final int PARSED = RevWalk.PARSED; + + private static final int SEEN = RevWalk.SEEN; + + private static final int UNINTERESTING = RevWalk.UNINTERESTING; + + /** + * Number of additional commits to scan after we think we are done. + * <p> + * This small buffer of commits is scanned to ensure we didn't miss anything + * as a result of clock skew when the commits were made. We need to set our + * constant to 1 additional commit due to the use of a pre-increment + * operator when accessing the value. + */ + static final int OVER_SCAN = 5 + 1; + + /** A commit near the end of time, to initialize {@link #last} with. */ + private static final RevCommit INIT_LAST; + + static { + INIT_LAST = new RevCommit(ObjectId.zeroId()); + INIT_LAST.commitTime = Integer.MAX_VALUE; + } + + private final RevWalk walker; + + private final DateRevQueue pending; + + private final RevFilter filter; + + private final int output; + + /** Last commit produced to the caller from {@link #next()}. */ + private RevCommit last = INIT_LAST; + + /** + * Number of commits we have remaining in our over-scan allotment. + * <p> + * Only relevant if there are {@link #UNINTERESTING} commits in the + * {@link #pending} queue. + */ + private int overScan = OVER_SCAN; + + boolean canDispose; + + PendingGenerator(final RevWalk w, final DateRevQueue p, + final RevFilter f, final int out) { + walker = w; + pending = p; + filter = f; + output = out; + canDispose = true; + } + + @Override + int outputType() { + return output | SORT_COMMIT_TIME_DESC; + } + + @Override + RevCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + try { + for (;;) { + final RevCommit c = pending.next(); + if (c == null) { + walker.curs.release(); + return null; + } + + final boolean produce; + if ((c.flags & UNINTERESTING) != 0) + produce = false; + else + produce = filter.include(walker, c); + + for (final RevCommit p : c.parents) { + if ((p.flags & SEEN) != 0) + continue; + if ((p.flags & PARSED) == 0) + p.parseHeaders(walker); + p.flags |= SEEN; + pending.add(p); + } + walker.carryFlagsImpl(c); + + if ((c.flags & UNINTERESTING) != 0) { + if (pending.everbodyHasFlag(UNINTERESTING)) { + final RevCommit n = pending.peek(); + if (n != null && n.commitTime >= last.commitTime) { + // This is too close to call. The next commit we + // would pop is dated after the last one produced. + // We have to keep going to ensure that we carry + // flags as much as necessary. + // + overScan = OVER_SCAN; + } else if (--overScan == 0) + throw StopWalkException.INSTANCE; + } else { + overScan = OVER_SCAN; + } + if (canDispose) + c.disposeBody(); + continue; + } + + if (produce) + return last = c; + else if (canDispose) + c.disposeBody(); + } + } catch (StopWalkException swe) { + walker.curs.release(); + pending.clear(); + return null; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevBlob.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevBlob.java new file mode 100644 index 0000000000..f4d46e7e6f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevBlob.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; + +/** A binary file, or a symbolic link. */ +public class RevBlob extends RevObject { + /** + * Create a new blob reference. + * + * @param id + * object name for the blob. + */ + protected RevBlob(final AnyObjectId id) { + super(id); + } + + @Override + public final int getType() { + return Constants.OBJ_BLOB; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java new file mode 100644 index 0000000000..1d2a49d3af --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java @@ -0,0 +1,529 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Commit; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.MutableObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.util.RawParseUtils; + +/** A commit reference to a commit in the DAG. */ +public class RevCommit extends RevObject { + static final RevCommit[] NO_PARENTS = {}; + + private RevTree tree; + + RevCommit[] parents; + + int commitTime; // An int here for performance, overflows in 2038 + + int inDegree; + + private byte[] buffer; + + /** + * Create a new commit reference. + * + * @param id + * object name for the commit. + */ + protected RevCommit(final AnyObjectId id) { + super(id); + } + + @Override + void parseHeaders(final RevWalk walk) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + parseCanonical(walk, loadCanonical(walk)); + } + + @Override + void parseBody(final RevWalk walk) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + if (buffer == null) { + buffer = loadCanonical(walk); + if ((flags & PARSED) == 0) + parseCanonical(walk, buffer); + } + } + + void parseCanonical(final RevWalk walk, final byte[] raw) { + final MutableObjectId idBuffer = walk.idBuffer; + idBuffer.fromString(raw, 5); + tree = walk.lookupTree(idBuffer); + + int ptr = 46; + if (parents == null) { + RevCommit[] pList = new RevCommit[1]; + int nParents = 0; + for (;;) { + if (raw[ptr] != 'p') + break; + idBuffer.fromString(raw, ptr + 7); + final RevCommit p = walk.lookupCommit(idBuffer); + if (nParents == 0) + pList[nParents++] = p; + else if (nParents == 1) { + pList = new RevCommit[] { pList[0], p }; + nParents = 2; + } else { + if (pList.length <= nParents) { + RevCommit[] old = pList; + pList = new RevCommit[pList.length + 32]; + System.arraycopy(old, 0, pList, 0, nParents); + } + pList[nParents++] = p; + } + ptr += 48; + } + if (nParents != pList.length) { + RevCommit[] old = pList; + pList = new RevCommit[nParents]; + System.arraycopy(old, 0, pList, 0, nParents); + } + parents = pList; + } + + // extract time from "committer " + ptr = RawParseUtils.committer(raw, ptr); + if (ptr > 0) { + ptr = RawParseUtils.nextLF(raw, ptr, '>'); + + // In 2038 commitTime will overflow unless it is changed to long. + commitTime = RawParseUtils.parseBase10(raw, ptr, null); + } + + if (walk.isRetainBody()) + buffer = raw; + flags |= PARSED; + } + + @Override + public final int getType() { + return Constants.OBJ_COMMIT; + } + + static void carryFlags(RevCommit c, final int carry) { + for (;;) { + final RevCommit[] pList = c.parents; + if (pList == null) + return; + final int n = pList.length; + if (n == 0) + return; + + for (int i = 1; i < n; i++) { + final RevCommit p = pList[i]; + if ((p.flags & carry) == carry) + continue; + p.flags |= carry; + carryFlags(p, carry); + } + + c = pList[0]; + if ((c.flags & carry) == carry) + return; + c.flags |= carry; + } + } + + /** + * Carry a RevFlag set on this commit to its parents. + * <p> + * If this commit is parsed, has parents, and has the supplied flag set on + * it we automatically add it to the parents, grand-parents, and so on until + * an unparsed commit or a commit with no parents is discovered. This + * permits applications to force a flag through the history chain when + * necessary. + * + * @param flag + * the single flag value to carry back onto parents. + */ + public void carry(final RevFlag flag) { + final int carry = flags & flag.mask; + if (carry != 0) + carryFlags(this, carry); + } + + /** + * Time from the "committer " line of the buffer. + * + * @return time, expressed as seconds since the epoch. + */ + public final int getCommitTime() { + return commitTime; + } + + /** + * Parse this commit buffer for display. + * + * @param walk + * revision walker owning this reference. + * @return parsed commit. + */ + public final Commit asCommit(final RevWalk walk) { + return new Commit(walk.db, this, buffer); + } + + /** + * Get a reference to this commit's tree. + * + * @return tree of this commit. + */ + public final RevTree getTree() { + return tree; + } + + /** + * Get the number of parent commits listed in this commit. + * + * @return number of parents; always a positive value but can be 0. + */ + public final int getParentCount() { + return parents.length; + } + + /** + * Get the nth parent from this commit's parent list. + * + * @param nth + * parent index to obtain. Must be in the range 0 through + * {@link #getParentCount()}-1. + * @return the specified parent. + * @throws ArrayIndexOutOfBoundsException + * an invalid parent index was specified. + */ + public final RevCommit getParent(final int nth) { + return parents[nth]; + } + + /** + * Obtain an array of all parents (<b>NOTE - THIS IS NOT A COPY</b>). + * <p> + * This method is exposed only to provide very fast, efficient access to + * this commit's parent list. Applications relying on this list should be + * very careful to ensure they do not modify its contents during their use + * of it. + * + * @return the array of parents. + */ + public final RevCommit[] getParents() { + return parents; + } + + /** + * Obtain the raw unparsed commit body (<b>NOTE - THIS IS NOT A COPY</b>). + * <p> + * This method is exposed only to provide very fast, efficient access to + * this commit's message buffer within a RevFilter. Applications relying on + * this buffer should be very careful to ensure they do not modify its + * contents during their use of it. + * + * @return the raw unparsed commit body. This is <b>NOT A COPY</b>. + * Altering the contents of this buffer may alter the walker's + * knowledge of this commit, and the results it produces. + */ + public final byte[] getRawBuffer() { + return buffer; + } + + /** + * Parse the author identity from the raw buffer. + * <p> + * This method parses and returns the content of the author line, after + * taking the commit's character set into account and decoding the author + * name and email address. This method is fairly expensive and produces a + * new PersonIdent instance on each invocation. Callers should invoke this + * method only if they are certain they will be outputting the result, and + * should cache the return value for as long as necessary to use all + * information from it. + * <p> + * RevFilter implementations should try to use {@link RawParseUtils} to scan + * the {@link #getRawBuffer()} instead, as this will allow faster evaluation + * of commits. + * + * @return identity of the author (name, email) and the time the commit was + * made by the author; null if no author line was found. + */ + public final PersonIdent getAuthorIdent() { + final byte[] raw = buffer; + final int nameB = RawParseUtils.author(raw, 0); + if (nameB < 0) + return null; + return RawParseUtils.parsePersonIdent(raw, nameB); + } + + /** + * Parse the committer identity from the raw buffer. + * <p> + * This method parses and returns the content of the committer line, after + * taking the commit's character set into account and decoding the committer + * name and email address. This method is fairly expensive and produces a + * new PersonIdent instance on each invocation. Callers should invoke this + * method only if they are certain they will be outputting the result, and + * should cache the return value for as long as necessary to use all + * information from it. + * <p> + * RevFilter implementations should try to use {@link RawParseUtils} to scan + * the {@link #getRawBuffer()} instead, as this will allow faster evaluation + * of commits. + * + * @return identity of the committer (name, email) and the time the commit + * was made by the committer; null if no committer line was found. + */ + public final PersonIdent getCommitterIdent() { + final byte[] raw = buffer; + final int nameB = RawParseUtils.committer(raw, 0); + if (nameB < 0) + return null; + return RawParseUtils.parsePersonIdent(raw, nameB); + } + + /** + * Parse the complete commit message and decode it to a string. + * <p> + * This method parses and returns the message portion of the commit buffer, + * after taking the commit's character set into account and decoding the + * buffer using that character set. This method is a fairly expensive + * operation and produces a new string on each invocation. + * + * @return decoded commit message as a string. Never null. + */ + public final String getFullMessage() { + final byte[] raw = buffer; + final int msgB = RawParseUtils.commitMessage(raw, 0); + if (msgB < 0) + return ""; + final Charset enc = RawParseUtils.parseEncoding(raw); + return RawParseUtils.decode(enc, raw, msgB, raw.length); + } + + /** + * Parse the commit message and return the first "line" of it. + * <p> + * The first line is everything up to the first pair of LFs. This is the + * "oneline" format, suitable for output in a single line display. + * <p> + * This method parses and returns the message portion of the commit buffer, + * after taking the commit's character set into account and decoding the + * buffer using that character set. This method is a fairly expensive + * operation and produces a new string on each invocation. + * + * @return decoded commit message as a string. Never null. The returned + * string does not contain any LFs, even if the first paragraph + * spanned multiple lines. Embedded LFs are converted to spaces. + */ + public final String getShortMessage() { + final byte[] raw = buffer; + final int msgB = RawParseUtils.commitMessage(raw, 0); + if (msgB < 0) + return ""; + + final Charset enc = RawParseUtils.parseEncoding(raw); + final int msgE = RawParseUtils.endOfParagraph(raw, msgB); + String str = RawParseUtils.decode(enc, raw, msgB, msgE); + if (hasLF(raw, msgB, msgE)) + str = str.replace('\n', ' '); + return str; + } + + static boolean hasLF(final byte[] r, int b, final int e) { + while (b < e) + if (r[b++] == '\n') + return true; + return false; + } + + /** + * Determine the encoding of the commit message buffer. + * <p> + * Locates the "encoding" header (if present) and then returns the proper + * character set to apply to this buffer to evaluate its contents as + * character data. + * <p> + * If no encoding header is present, {@link Constants#CHARSET} is assumed. + * + * @return the preferred encoding of {@link #getRawBuffer()}. + */ + public final Charset getEncoding() { + return RawParseUtils.parseEncoding(buffer); + } + + /** + * Parse the footer lines (e.g. "Signed-off-by") for machine processing. + * <p> + * This method splits all of the footer lines out of the last paragraph of + * the commit message, providing each line as a key-value pair, ordered by + * the order of the line's appearance in the commit message itself. + * <p> + * A footer line's key must match the pattern {@code ^[A-Za-z0-9-]+:}, while + * the value is free-form, but must not contain an LF. Very common keys seen + * in the wild are: + * <ul> + * <li>{@code Signed-off-by} (agrees to Developer Certificate of Origin) + * <li>{@code Acked-by} (thinks change looks sane in context) + * <li>{@code Reported-by} (originally found the issue this change fixes) + * <li>{@code Tested-by} (validated change fixes the issue for them) + * <li>{@code CC}, {@code Cc} (copy on all email related to this change) + * <li>{@code Bug} (link to project's bug tracking system) + * </ul> + * + * @return ordered list of footer lines; empty list if no footers found. + */ + public final List<FooterLine> getFooterLines() { + final byte[] raw = buffer; + int ptr = raw.length - 1; + while (raw[ptr] == '\n') // trim any trailing LFs, not interesting + ptr--; + + final int msgB = RawParseUtils.commitMessage(raw, 0); + final ArrayList<FooterLine> r = new ArrayList<FooterLine>(4); + final Charset enc = getEncoding(); + for (;;) { + ptr = RawParseUtils.prevLF(raw, ptr); + if (ptr <= msgB) + break; // Don't parse commit headers as footer lines. + + final int keyStart = ptr + 2; + if (raw[keyStart] == '\n') + break; // Stop at first paragraph break, no footers above it. + + final int keyEnd = RawParseUtils.endOfFooterLineKey(raw, keyStart); + if (keyEnd < 0) + continue; // Not a well formed footer line, skip it. + + // Skip over the ': *' at the end of the key before the value. + // + int valStart = keyEnd + 1; + while (valStart < raw.length && raw[valStart] == ' ') + valStart++; + + // Value ends at the LF, and does not include it. + // + int valEnd = RawParseUtils.nextLF(raw, valStart); + if (raw[valEnd - 1] == '\n') + valEnd--; + + r.add(new FooterLine(raw, enc, keyStart, keyEnd, valStart, valEnd)); + } + Collections.reverse(r); + return r; + } + + /** + * Get the values of all footer lines with the given key. + * + * @param keyName + * footer key to find values of, case insensitive. + * @return values of footers with key of {@code keyName}, ordered by their + * order of appearance. Duplicates may be returned if the same + * footer appeared more than once. Empty list if no footers appear + * with the specified key, or there are no footers at all. + * @see #getFooterLines() + */ + public final List<String> getFooterLines(final String keyName) { + return getFooterLines(new FooterKey(keyName)); + } + + /** + * Get the values of all footer lines with the given key. + * + * @param keyName + * footer key to find values of, case insensitive. + * @return values of footers with key of {@code keyName}, ordered by their + * order of appearance. Duplicates may be returned if the same + * footer appeared more than once. Empty list if no footers appear + * with the specified key, or there are no footers at all. + * @see #getFooterLines() + */ + public final List<String> getFooterLines(final FooterKey keyName) { + final List<FooterLine> src = getFooterLines(); + if (src.isEmpty()) + return Collections.emptyList(); + final ArrayList<String> r = new ArrayList<String>(src.size()); + for (final FooterLine f : src) { + if (f.matches(keyName)) + r.add(f.getValue()); + } + return r; + } + + /** + * Reset this commit to allow another RevWalk with the same instances. + * <p> + * Subclasses <b>must</b> call <code>super.reset()</code> to ensure the + * basic information can be correctly cleared out. + */ + public void reset() { + inDegree = 0; + } + + final void disposeBody() { + buffer = null; + } + + @Override + public String toString() { + final StringBuilder s = new StringBuilder(); + s.append(Constants.typeString(getType())); + s.append(' '); + s.append(name()); + s.append(' '); + s.append(commitTime); + s.append(' '); + appendCoreFlags(s); + return s.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommitList.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommitList.java new file mode 100644 index 0000000000..d6abccfba4 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommitList.java @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.revwalk.filter.RevFilter; + +/** + * An ordered list of {@link RevCommit} subclasses. + * + * @param <E> + * type of subclass of RevCommit the list is storing. + */ +public class RevCommitList<E extends RevCommit> extends RevObjectList<E> { + private RevWalk walker; + + @Override + public void clear() { + super.clear(); + walker = null; + } + + /** + * Apply a flag to all commits matching the specified filter. + * <p> + * Same as <code>applyFlag(matching, flag, 0, size())</code>, but without + * the incremental behavior. + * + * @param matching + * the filter to test commits with. If the filter includes a + * commit it will have the flag set; if the filter does not + * include the commit the flag will be unset. + * @param flag + * the flag to apply (or remove). Applications are responsible + * for allocating this flag from the source RevWalk. + * @throws IOException + * revision filter needed to read additional objects, but an + * error occurred while reading the pack files or loose objects + * of the repository. + * @throws IncorrectObjectTypeException + * revision filter needed to read additional objects, but an + * object was not of the correct type. Repository corruption may + * have occurred. + * @throws MissingObjectException + * revision filter needed to read additional objects, but an + * object that should be present was not found. Repository + * corruption may have occurred. + */ + public void applyFlag(final RevFilter matching, final RevFlag flag) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + applyFlag(matching, flag, 0, size()); + } + + /** + * Apply a flag to all commits matching the specified filter. + * <p> + * This version allows incremental testing and application, such as from a + * background thread that needs to periodically halt processing and send + * updates to the UI. + * + * @param matching + * the filter to test commits with. If the filter includes a + * commit it will have the flag set; if the filter does not + * include the commit the flag will be unset. + * @param flag + * the flag to apply (or remove). Applications are responsible + * for allocating this flag from the source RevWalk. + * @param rangeBegin + * first commit within the list to begin testing at, inclusive. + * Must not be negative, but may be beyond the end of the list. + * @param rangeEnd + * last commit within the list to end testing at, exclusive. If + * smaller than or equal to <code>rangeBegin</code> then no + * commits will be tested. + * @throws IOException + * revision filter needed to read additional objects, but an + * error occurred while reading the pack files or loose objects + * of the repository. + * @throws IncorrectObjectTypeException + * revision filter needed to read additional objects, but an + * object was not of the correct type. Repository corruption may + * have occurred. + * @throws MissingObjectException + * revision filter needed to read additional objects, but an + * object that should be present was not found. Repository + * corruption may have occurred. + */ + public void applyFlag(final RevFilter matching, final RevFlag flag, + int rangeBegin, int rangeEnd) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + final RevWalk w = flag.getRevWalk(); + rangeEnd = Math.min(rangeEnd, size()); + while (rangeBegin < rangeEnd) { + int index = rangeBegin; + Block s = contents; + while (s.shift > 0) { + final int i = index >> s.shift; + index -= i << s.shift; + s = (Block) s.contents[i]; + } + + while (rangeBegin++ < rangeEnd && index < BLOCK_SIZE) { + final RevCommit c = (RevCommit) s.contents[index++]; + if (matching.include(w, c)) + c.add(flag); + else + c.remove(flag); + } + } + } + + /** + * Remove the given flag from all commits. + * <p> + * Same as <code>clearFlag(flag, 0, size())</code>, but without the + * incremental behavior. + * + * @param flag + * the flag to remove. Applications are responsible for + * allocating this flag from the source RevWalk. + */ + public void clearFlag(final RevFlag flag) { + clearFlag(flag, 0, size()); + } + + /** + * Remove the given flag from all commits. + * <p> + * This method is actually implemented in terms of: + * <code>applyFlag(RevFilter.NONE, flag, rangeBegin, rangeEnd)</code>. + * + * @param flag + * the flag to remove. Applications are responsible for + * allocating this flag from the source RevWalk. + * @param rangeBegin + * first commit within the list to begin testing at, inclusive. + * Must not be negative, but may be beyond the end of the list. + * @param rangeEnd + * last commit within the list to end testing at, exclusive. If + * smaller than or equal to <code>rangeBegin</code> then no + * commits will be tested. + */ + public void clearFlag(final RevFlag flag, final int rangeBegin, + final int rangeEnd) { + try { + applyFlag(RevFilter.NONE, flag, rangeBegin, rangeEnd); + } catch (IOException e) { + // Never happen. The filter we use does not throw any + // exceptions, for any reason. + } + } + + /** + * Find the next commit that has the given flag set. + * + * @param flag + * the flag to test commits against. + * @param begin + * first commit index to test at. Applications may wish to begin + * at 0, to test the first commit in the list. + * @return index of the first commit at or after index <code>begin</code> + * that has the specified flag set on it; -1 if no match is found. + */ + public int indexOf(final RevFlag flag, int begin) { + while (begin < size()) { + int index = begin; + Block s = contents; + while (s.shift > 0) { + final int i = index >> s.shift; + index -= i << s.shift; + s = (Block) s.contents[i]; + } + + while (begin++ < size() && index < BLOCK_SIZE) { + final RevCommit c = (RevCommit) s.contents[index++]; + if (c.has(flag)) + return begin; + } + } + return -1; + } + + /** + * Find the next commit that has the given flag set. + * + * @param flag + * the flag to test commits against. + * @param begin + * first commit index to test at. Applications may wish to begin + * at <code>size()-1</code>, to test the last commit in the + * list. + * @return index of the first commit at or before index <code>begin</code> + * that has the specified flag set on it; -1 if no match is found. + */ + public int lastIndexOf(final RevFlag flag, int begin) { + begin = Math.min(begin, size() - 1); + while (begin >= 0) { + int index = begin; + Block s = contents; + while (s.shift > 0) { + final int i = index >> s.shift; + index -= i << s.shift; + s = (Block) s.contents[i]; + } + + while (begin-- >= 0 && index >= 0) { + final RevCommit c = (RevCommit) s.contents[index--]; + if (c.has(flag)) + return begin; + } + } + return -1; + } + + /** + * Set the revision walker this list populates itself from. + * + * @param w + * the walker to populate from. + * @see #fillTo(int) + */ + public void source(final RevWalk w) { + walker = w; + } + + /** + * Is this list still pending more items? + * + * @return true if {@link #fillTo(int)} might be able to extend the list + * size when called. + */ + public boolean isPending() { + return walker != null; + } + + /** + * Ensure this list contains at least a specified number of commits. + * <p> + * The revision walker specified by {@link #source(RevWalk)} is pumped until + * the given number of commits are contained in this list. If there are + * fewer total commits available from the walk then the method will return + * early. Callers can test the final size of the list by {@link #size()} to + * determine if the high water mark specified was met. + * + * @param highMark + * number of commits the caller wants this list to contain when + * the fill operation is complete. + * @throws IOException + * see {@link RevWalk#next()} + * @throws IncorrectObjectTypeException + * see {@link RevWalk#next()} + * @throws MissingObjectException + * see {@link RevWalk#next()} + */ + public void fillTo(final int highMark) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + if (walker == null || size > highMark) + return; + + Generator p = walker.pending; + RevCommit c = p.next(); + if (c == null) { + walker.pending = EndGenerator.INSTANCE; + walker = null; + return; + } + enter(size, (E) c); + add((E) c); + p = walker.pending; + + while (size <= highMark) { + int index = size; + Block s = contents; + while (index >> s.shift >= BLOCK_SIZE) { + s = new Block(s.shift + BLOCK_SHIFT); + s.contents[0] = contents; + contents = s; + } + while (s.shift > 0) { + final int i = index >> s.shift; + index -= i << s.shift; + if (s.contents[i] == null) + s.contents[i] = new Block(s.shift - BLOCK_SHIFT); + s = (Block) s.contents[i]; + } + + final Object[] dst = s.contents; + while (size <= highMark && index < BLOCK_SIZE) { + c = p.next(); + if (c == null) { + walker.pending = EndGenerator.INSTANCE; + walker = null; + return; + } + enter(size++, (E) c); + dst[index++] = c; + } + } + } + + /** + * Optional callback invoked when commits enter the list by fillTo. + * <p> + * This method is only called during {@link #fillTo(int)}. + * + * @param index + * the list position this object will appear at. + * @param e + * the object being added (or set) into the list. + */ + protected void enter(final int index, final E e) { + // Do nothing by default. + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevFlag.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevFlag.java new file mode 100644 index 0000000000..a8d644c335 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevFlag.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +/** + * Application level mark bit for {@link RevObject}s. + * <p> + * To create a flag use {@link RevWalk#newFlag(String)}. + */ +public class RevFlag { + /** + * Uninteresting by {@link RevWalk#markUninteresting(RevCommit)}. + * <p> + * We flag commits as uninteresting if the caller does not want commits + * reachable from a commit to {@link RevWalk#markUninteresting(RevCommit)}. + * This flag is always carried into the commit's parents and is a key part + * of the "rev-list B --not A" feature; A is marked UNINTERESTING. + * <p> + * This is a static flag. Its RevWalk is not available. + */ + public static final RevFlag UNINTERESTING = new StaticRevFlag( + "UNINTERESTING", RevWalk.UNINTERESTING); + + final RevWalk walker; + + final String name; + + final int mask; + + RevFlag(final RevWalk w, final String n, final int m) { + walker = w; + name = n; + mask = m; + } + + /** + * Get the revision walk instance this flag was created from. + * + * @return the walker this flag was allocated out of, and belongs to. + */ + public RevWalk getRevWalk() { + return walker; + } + + public String toString() { + return name; + } + + static class StaticRevFlag extends RevFlag { + StaticRevFlag(final String n, final int m) { + super(null, n, m); + } + + @Override + public RevWalk getRevWalk() { + throw new UnsupportedOperationException(toString() + + " is a static flag and has no RevWalk instance"); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevFlagSet.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevFlagSet.java new file mode 100644 index 0000000000..fb9b4525a9 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevFlagSet.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +/** + * Multiple application level mark bits for {@link RevObject}s. + * + * @see RevFlag + */ +public class RevFlagSet extends AbstractSet<RevFlag> { + int mask; + + private final List<RevFlag> active; + + /** Create an empty set of flags. */ + public RevFlagSet() { + active = new ArrayList<RevFlag>(); + } + + /** + * Create a set of flags, copied from an existing set. + * + * @param s + * the set to copy flags from. + */ + public RevFlagSet(final RevFlagSet s) { + mask = s.mask; + active = new ArrayList<RevFlag>(s.active); + } + + /** + * Create a set of flags, copied from an existing collection. + * + * @param s + * the collection to copy flags from. + */ + public RevFlagSet(final Collection<RevFlag> s) { + this(); + addAll(s); + } + + @Override + public boolean contains(final Object o) { + if (o instanceof RevFlag) + return (mask & ((RevFlag) o).mask) != 0; + return false; + } + + @Override + public boolean containsAll(final Collection<?> c) { + if (c instanceof RevFlagSet) { + final int cMask = ((RevFlagSet) c).mask; + return (mask & cMask) == cMask; + } + return super.containsAll(c); + } + + @Override + public boolean add(final RevFlag flag) { + if ((mask & flag.mask) != 0) + return false; + mask |= flag.mask; + int p = 0; + while (p < active.size() && active.get(p).mask < flag.mask) + p++; + active.add(p, flag); + return true; + } + + @Override + public boolean remove(final Object o) { + final RevFlag flag = (RevFlag) o; + if ((mask & flag.mask) == 0) + return false; + mask &= ~flag.mask; + for (int i = 0; i < active.size(); i++) + if (active.get(i).mask == flag.mask) + active.remove(i); + return true; + } + + @Override + public Iterator<RevFlag> iterator() { + final Iterator<RevFlag> i = active.iterator(); + return new Iterator<RevFlag>() { + private RevFlag current; + + public boolean hasNext() { + return i.hasNext(); + } + + public RevFlag next() { + return current = i.next(); + } + + public void remove() { + mask &= ~current.mask; + i.remove(); + } + }; + } + + @Override + public int size() { + return active.size(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObject.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObject.java new file mode 100644 index 0000000000..e5e3abcade --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObject.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; + +/** Base object type accessed during revision walking. */ +public abstract class RevObject extends ObjectId { + static final int PARSED = 1; + + int flags; + + RevObject(final AnyObjectId name) { + super(name); + } + + void parseHeaders(final RevWalk walk) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + loadCanonical(walk); + flags |= PARSED; + } + + void parseBody(final RevWalk walk) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + if ((flags & PARSED) == 0) + parseHeaders(walk); + } + + final byte[] loadCanonical(final RevWalk walk) throws IOException, + MissingObjectException, IncorrectObjectTypeException, + CorruptObjectException { + final ObjectLoader ldr = walk.db.openObject(walk.curs, this); + if (ldr == null) + throw new MissingObjectException(this, getType()); + final byte[] data = ldr.getCachedBytes(); + if (getType() != ldr.getType()) + throw new IncorrectObjectTypeException(this, getType()); + return data; + } + + /** + * Get Git object type. See {@link Constants}. + * + * @return object type + */ + public abstract int getType(); + + /** + * Get the name of this object. + * + * @return unique hash of this object. + */ + public final ObjectId getId() { + return this; + } + + @Override + public final boolean equals(final AnyObjectId o) { + return this == o; + } + + @Override + public final boolean equals(final Object o) { + return this == o; + } + + /** + * Test to see if the flag has been set on this object. + * + * @param flag + * the flag to test. + * @return true if the flag has been added to this object; false if not. + */ + public final boolean has(final RevFlag flag) { + return (flags & flag.mask) != 0; + } + + /** + * Test to see if any flag in the set has been set on this object. + * + * @param set + * the flags to test. + * @return true if any flag in the set has been added to this object; false + * if not. + */ + public final boolean hasAny(final RevFlagSet set) { + return (flags & set.mask) != 0; + } + + /** + * Test to see if all flags in the set have been set on this object. + * + * @param set + * the flags to test. + * @return true if all flags of the set have been added to this object; + * false if some or none have been added. + */ + public final boolean hasAll(final RevFlagSet set) { + return (flags & set.mask) == set.mask; + } + + /** + * Add a flag to this object. + * <p> + * If the flag is already set on this object then the method has no effect. + * + * @param flag + * the flag to mark on this object, for later testing. + */ + public final void add(final RevFlag flag) { + flags |= flag.mask; + } + + /** + * Add a set of flags to this object. + * + * @param set + * the set of flags to mark on this object, for later testing. + */ + public final void add(final RevFlagSet set) { + flags |= set.mask; + } + + /** + * Remove a flag from this object. + * <p> + * If the flag is not set on this object then the method has no effect. + * + * @param flag + * the flag to remove from this object. + */ + public final void remove(final RevFlag flag) { + flags &= ~flag.mask; + } + + /** + * Remove a set of flags from this object. + * + * @param set + * the flag to remove from this object. + */ + public final void remove(final RevFlagSet set) { + flags &= ~set.mask; + } + + @Override + public String toString() { + final StringBuilder s = new StringBuilder(); + s.append(Constants.typeString(getType())); + s.append(' '); + s.append(name()); + s.append(' '); + appendCoreFlags(s); + return s.toString(); + } + + /** + * @param s + * buffer to append a debug description of core RevFlags onto. + */ + protected void appendCoreFlags(final StringBuilder s) { + s.append((flags & RevWalk.TOPO_DELAY) != 0 ? 'o' : '-'); + s.append((flags & RevWalk.TEMP_MARK) != 0 ? 't' : '-'); + s.append((flags & RevWalk.REWRITE) != 0 ? 'r' : '-'); + s.append((flags & RevWalk.UNINTERESTING) != 0 ? 'u' : '-'); + s.append((flags & RevWalk.SEEN) != 0 ? 's' : '-'); + s.append((flags & RevWalk.PARSED) != 0 ? 'p' : '-'); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObjectList.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObjectList.java new file mode 100644 index 0000000000..3ae1a71f1b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevObjectList.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.util.AbstractList; + +/** + * An ordered list of {@link RevObject} subclasses. + * + * @param <E> + * type of subclass of RevObject the list is storing. + */ +public class RevObjectList<E extends RevObject> extends AbstractList<E> { + static final int BLOCK_SHIFT = 8; + + static final int BLOCK_SIZE = 1 << BLOCK_SHIFT; + + Block contents; + + int size; + + /** Create an empty object list. */ + public RevObjectList() { + clear(); + } + + public void add(final int index, final E element) { + if (index != size) + throw new UnsupportedOperationException("Not add-at-end: " + index); + set(index, element); + size++; + } + + public E set(int index, E element) { + Block s = contents; + while (index >> s.shift >= BLOCK_SIZE) { + s = new Block(s.shift + BLOCK_SHIFT); + s.contents[0] = contents; + contents = s; + } + while (s.shift > 0) { + final int i = index >> s.shift; + index -= i << s.shift; + if (s.contents[i] == null) + s.contents[i] = new Block(s.shift - BLOCK_SHIFT); + s = (Block) s.contents[i]; + } + final Object old = s.contents[index]; + s.contents[index] = element; + return (E) old; + } + + public E get(int index) { + Block s = contents; + if (index >> s.shift >= 1024) + return null; + while (s != null && s.shift > 0) { + final int i = index >> s.shift; + index -= i << s.shift; + s = (Block) s.contents[i]; + } + return s != null ? (E) s.contents[index] : null; + } + + public int size() { + return size; + } + + @Override + public void clear() { + contents = new Block(0); + size = 0; + } + + static class Block { + final Object[] contents = new Object[BLOCK_SIZE]; + + final int shift; + + Block(final int s) { + shift = s; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevSort.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevSort.java new file mode 100644 index 0000000000..238af12fdb --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevSort.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +/** Sorting strategies supported by {@link RevWalk} and {@link ObjectWalk}. */ +public enum RevSort { + /** + * No specific sorting is requested. + * <p> + * Applications should not rely upon the ordering produced by this strategy. + * Any ordering in the output is caused by low level implementation details + * and may change without notice. + */ + NONE, + + /** + * Sort by commit time, descending (newest first, oldest last). + * <p> + * This strategy can be combined with {@link #TOPO}. + */ + COMMIT_TIME_DESC, + + /** + * Topological sorting (all children before parents). + * <p> + * This strategy can be combined with {@link #COMMIT_TIME_DESC}. + */ + TOPO, + + /** + * Flip the output into the reverse ordering. + * <p> + * This strategy can be combined with the others described by this type as + * it is usually performed at the very end. + */ + REVERSE, + + /** + * Include {@link RevFlag#UNINTERESTING} boundary commits after all others. + * In {@link ObjectWalk}, objects associated with such commits (trees, + * blobs), and all other objects marked explicitly as UNINTERESTING are also + * included. + * <p> + * A boundary commit is a UNINTERESTING parent of an interesting commit that + * was previously output. + */ + BOUNDARY; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java new file mode 100644 index 0000000000..a77757dc2c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; +import java.nio.charset.Charset; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Tag; +import org.eclipse.jgit.util.MutableInteger; +import org.eclipse.jgit.util.RawParseUtils; + +/** An annotated tag. */ +public class RevTag extends RevObject { + private RevObject object; + + private byte[] buffer; + + private String tagName; + + /** + * Create a new tag reference. + * + * @param id + * object name for the tag. + */ + protected RevTag(final AnyObjectId id) { + super(id); + } + + @Override + void parseHeaders(final RevWalk walk) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + parseCanonical(walk, loadCanonical(walk)); + } + + @Override + void parseBody(final RevWalk walk) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + if (buffer == null) { + buffer = loadCanonical(walk); + if ((flags & PARSED) == 0) + parseCanonical(walk, buffer); + } + } + + void parseCanonical(final RevWalk walk, final byte[] rawTag) + throws CorruptObjectException { + final MutableInteger pos = new MutableInteger(); + final int oType; + + pos.value = 53; // "object $sha1\ntype " + oType = Constants.decodeTypeString(this, rawTag, (byte) '\n', pos); + walk.idBuffer.fromString(rawTag, 7); + object = walk.lookupAny(walk.idBuffer, oType); + + int p = pos.value += 4; // "tag " + final int nameEnd = RawParseUtils.nextLF(rawTag, p) - 1; + tagName = RawParseUtils.decode(Constants.CHARSET, rawTag, p, nameEnd); + + if (walk.isRetainBody()) + buffer = rawTag; + flags |= PARSED; + } + + @Override + public final int getType() { + return Constants.OBJ_TAG; + } + + /** + * Parse the tagger identity from the raw buffer. + * <p> + * This method parses and returns the content of the tagger line, after + * taking the tag's character set into account and decoding the tagger + * name and email address. This method is fairly expensive and produces a + * new PersonIdent instance on each invocation. Callers should invoke this + * method only if they are certain they will be outputting the result, and + * should cache the return value for as long as necessary to use all + * information from it. + * + * @return identity of the tagger (name, email) and the time the tag + * was made by the tagger; null if no tagger line was found. + */ + public final PersonIdent getTaggerIdent() { + final byte[] raw = buffer; + final int nameB = RawParseUtils.tagger(raw, 0); + if (nameB < 0) + return null; + return RawParseUtils.parsePersonIdent(raw, nameB); + } + + /** + * Parse the complete tag message and decode it to a string. + * <p> + * This method parses and returns the message portion of the tag buffer, + * after taking the tag's character set into account and decoding the buffer + * using that character set. This method is a fairly expensive operation and + * produces a new string on each invocation. + * + * @return decoded tag message as a string. Never null. + */ + public final String getFullMessage() { + final byte[] raw = buffer; + final int msgB = RawParseUtils.tagMessage(raw, 0); + if (msgB < 0) + return ""; + final Charset enc = RawParseUtils.parseEncoding(raw); + return RawParseUtils.decode(enc, raw, msgB, raw.length); + } + + /** + * Parse the tag message and return the first "line" of it. + * <p> + * The first line is everything up to the first pair of LFs. This is the + * "oneline" format, suitable for output in a single line display. + * <p> + * This method parses and returns the message portion of the tag buffer, + * after taking the tag's character set into account and decoding the buffer + * using that character set. This method is a fairly expensive operation and + * produces a new string on each invocation. + * + * @return decoded tag message as a string. Never null. The returned string + * does not contain any LFs, even if the first paragraph spanned + * multiple lines. Embedded LFs are converted to spaces. + */ + public final String getShortMessage() { + final byte[] raw = buffer; + final int msgB = RawParseUtils.tagMessage(raw, 0); + if (msgB < 0) + return ""; + + final Charset enc = RawParseUtils.parseEncoding(raw); + final int msgE = RawParseUtils.endOfParagraph(raw, msgB); + String str = RawParseUtils.decode(enc, raw, msgB, msgE); + if (RevCommit.hasLF(raw, msgB, msgE)) + str = str.replace('\n', ' '); + return str; + } + + /** + * Parse this tag buffer for display. + * + * @param walk + * revision walker owning this reference. + * @return parsed tag. + */ + public Tag asTag(final RevWalk walk) { + return new Tag(walk.db, this, tagName, buffer); + } + + /** + * Get a reference to the object this tag was placed on. + * + * @return object this tag refers to. + */ + public final RevObject getObject() { + return object; + } + + /** + * Get the name of this tag, from the tag header. + * + * @return name of the tag, according to the tag header. + */ + public final String getTagName() { + return tagName; + } + + final void disposeBody() { + buffer = null; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTree.java new file mode 100644 index 0000000000..4fa153a657 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTree.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; + +/** A reference to a tree of subtrees/files. */ +public class RevTree extends RevObject { + /** + * Create a new tree reference. + * + * @param id + * object name for the tree. + */ + protected RevTree(final AnyObjectId id) { + super(id); + } + + @Override + public final int getType() { + return Constants.OBJ_TREE; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java new file mode 100644 index 0000000000..ac2643422f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java @@ -0,0 +1,1085 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.Iterator; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.RevWalkException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.MutableObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdSubclassMap; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.WindowCursor; +import org.eclipse.jgit.revwalk.filter.RevFilter; +import org.eclipse.jgit.treewalk.filter.TreeFilter; + +/** + * Walks a commit graph and produces the matching commits in order. + * <p> + * A RevWalk instance can only be used once to generate results. Running a + * second time requires creating a new RevWalk instance, or invoking + * {@link #reset()} before starting again. Resetting an existing instance may be + * faster for some applications as commit body parsing can be avoided on the + * later invocations. + * <p> + * RevWalk instances are not thread-safe. Applications must either restrict + * usage of a RevWalk instance to a single thread, or implement their own + * synchronization at a higher level. + * <p> + * Multiple simultaneous RevWalk instances per {@link Repository} are permitted, + * even from concurrent threads. Equality of {@link RevCommit}s from two + * different RevWalk instances is never true, even if their {@link ObjectId}s + * are equal (and thus they describe the same commit). + * <p> + * The offered iterator is over the list of RevCommits described by the + * configuration of this instance. Applications should restrict themselves to + * using either the provided Iterator or {@link #next()}, but never use both on + * the same RevWalk at the same time. The Iterator may buffer RevCommits, while + * {@link #next()} does not. + */ +public class RevWalk implements Iterable<RevCommit> { + /** + * Set on objects whose important header data has been loaded. + * <p> + * For a RevCommit this indicates we have pulled apart the tree and parent + * references from the raw bytes available in the repository and translated + * those to our own local RevTree and RevCommit instances. The raw buffer is + * also available for message and other header filtering. + * <p> + * For a RevTag this indicates we have pulled part the tag references to + * find out who the tag refers to, and what that object's type is. + */ + static final int PARSED = 1 << 0; + + /** + * Set on RevCommit instances added to our {@link #pending} queue. + * <p> + * We use this flag to avoid adding the same commit instance twice to our + * queue, especially if we reached it by more than one path. + */ + static final int SEEN = 1 << 1; + + /** + * Set on RevCommit instances the caller does not want output. + * <p> + * We flag commits as uninteresting if the caller does not want commits + * reachable from a commit given to {@link #markUninteresting(RevCommit)}. + * This flag is always carried into the commit's parents and is a key part + * of the "rev-list B --not A" feature; A is marked UNINTERESTING. + */ + static final int UNINTERESTING = 1 << 2; + + /** + * Set on a RevCommit that can collapse out of the history. + * <p> + * If the {@link #treeFilter} concluded that this commit matches his + * parents' for all of the paths that the filter is interested in then we + * mark the commit REWRITE. Later we can rewrite the parents of a REWRITE + * child to remove chains of REWRITE commits before we produce the child to + * the application. + * + * @see RewriteGenerator + */ + static final int REWRITE = 1 << 3; + + /** + * Temporary mark for use within generators or filters. + * <p> + * This mark is only for local use within a single scope. If someone sets + * the mark they must unset it before any other code can see the mark. + */ + static final int TEMP_MARK = 1 << 4; + + /** + * Temporary mark for use within {@link TopoSortGenerator}. + * <p> + * This mark indicates the commit could not produce when it wanted to, as at + * least one child was behind it. Commits with this flag are delayed until + * all children have been output first. + */ + static final int TOPO_DELAY = 1 << 5; + + /** Number of flag bits we keep internal for our own use. See above flags. */ + static final int RESERVED_FLAGS = 6; + + private static final int APP_FLAGS = -1 & ~((1 << RESERVED_FLAGS) - 1); + + final Repository db; + + final WindowCursor curs; + + final MutableObjectId idBuffer; + + private final ObjectIdSubclassMap<RevObject> objects; + + private int freeFlags = APP_FLAGS; + + private int delayFreeFlags; + + int carryFlags = UNINTERESTING; + + private final ArrayList<RevCommit> roots; + + AbstractRevQueue queue; + + Generator pending; + + private final EnumSet<RevSort> sorting; + + private RevFilter filter; + + private TreeFilter treeFilter; + + private boolean retainBody; + + /** + * Create a new revision walker for a given repository. + * + * @param repo + * the repository the walker will obtain data from. + */ + public RevWalk(final Repository repo) { + db = repo; + curs = new WindowCursor(); + idBuffer = new MutableObjectId(); + objects = new ObjectIdSubclassMap<RevObject>(); + roots = new ArrayList<RevCommit>(); + queue = new DateRevQueue(); + pending = new StartGenerator(this); + sorting = EnumSet.of(RevSort.NONE); + filter = RevFilter.ALL; + treeFilter = TreeFilter.ALL; + retainBody = true; + } + + /** + * Get the repository this walker loads objects from. + * + * @return the repository this walker was created to read. + */ + public Repository getRepository() { + return db; + } + + /** + * Mark a commit to start graph traversal from. + * <p> + * Callers are encouraged to use {@link #parseCommit(AnyObjectId)} to obtain + * the commit reference, rather than {@link #lookupCommit(AnyObjectId)}, as + * this method requires the commit to be parsed before it can be added as a + * root for the traversal. + * <p> + * The method will automatically parse an unparsed commit, but error + * handling may be more difficult for the application to explain why a + * RevCommit is not actually a commit. The object pool of this walker would + * also be 'poisoned' by the non-commit RevCommit. + * + * @param c + * the commit to start traversing from. The commit passed must be + * from this same revision walker. + * @throws MissingObjectException + * the commit supplied is not available from the object + * database. This usually indicates the supplied commit is + * invalid, but the reference was constructed during an earlier + * invocation to {@link #lookupCommit(AnyObjectId)}. + * @throws IncorrectObjectTypeException + * the object was not parsed yet and it was discovered during + * parsing that it is not actually a commit. This usually + * indicates the caller supplied a non-commit SHA-1 to + * {@link #lookupCommit(AnyObjectId)}. + * @throws IOException + * a pack file or loose object could not be read. + */ + public void markStart(final RevCommit c) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + if ((c.flags & SEEN) != 0) + return; + if ((c.flags & PARSED) == 0) + c.parseHeaders(this); + c.flags |= SEEN; + roots.add(c); + queue.add(c); + } + + /** + * Mark commits to start graph traversal from. + * + * @param list + * commits to start traversing from. The commits passed must be + * from this same revision walker. + * @throws MissingObjectException + * one of the commits supplied is not available from the object + * database. This usually indicates the supplied commit is + * invalid, but the reference was constructed during an earlier + * invocation to {@link #lookupCommit(AnyObjectId)}. + * @throws IncorrectObjectTypeException + * the object was not parsed yet and it was discovered during + * parsing that it is not actually a commit. This usually + * indicates the caller supplied a non-commit SHA-1 to + * {@link #lookupCommit(AnyObjectId)}. + * @throws IOException + * a pack file or loose object could not be read. + */ + public void markStart(final Collection<RevCommit> list) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + for (final RevCommit c : list) + markStart(c); + } + + /** + * Mark a commit to not produce in the output. + * <p> + * Uninteresting commits denote not just themselves but also their entire + * ancestry chain, back until the merge base of an uninteresting commit and + * an otherwise interesting commit. + * <p> + * Callers are encouraged to use {@link #parseCommit(AnyObjectId)} to obtain + * the commit reference, rather than {@link #lookupCommit(AnyObjectId)}, as + * this method requires the commit to be parsed before it can be added as a + * root for the traversal. + * <p> + * The method will automatically parse an unparsed commit, but error + * handling may be more difficult for the application to explain why a + * RevCommit is not actually a commit. The object pool of this walker would + * also be 'poisoned' by the non-commit RevCommit. + * + * @param c + * the commit to start traversing from. The commit passed must be + * from this same revision walker. + * @throws MissingObjectException + * the commit supplied is not available from the object + * database. This usually indicates the supplied commit is + * invalid, but the reference was constructed during an earlier + * invocation to {@link #lookupCommit(AnyObjectId)}. + * @throws IncorrectObjectTypeException + * the object was not parsed yet and it was discovered during + * parsing that it is not actually a commit. This usually + * indicates the caller supplied a non-commit SHA-1 to + * {@link #lookupCommit(AnyObjectId)}. + * @throws IOException + * a pack file or loose object could not be read. + */ + public void markUninteresting(final RevCommit c) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + c.flags |= UNINTERESTING; + carryFlagsImpl(c); + markStart(c); + } + + /** + * Determine if a commit is reachable from another commit. + * <p> + * A commit <code>base</code> is an ancestor of <code>tip</code> if we + * can find a path of commits that leads from <code>tip</code> and ends at + * <code>base</code>. + * <p> + * This utility function resets the walker, inserts the two supplied + * commits, and then executes a walk until an answer can be obtained. + * Currently allocated RevFlags that have been added to RevCommit instances + * will be retained through the reset. + * + * @param base + * commit the caller thinks is reachable from <code>tip</code>. + * @param tip + * commit to start iteration from, and which is most likely a + * descendant (child) of <code>base</code>. + * @return true if there is a path directly from <code>tip</code> to + * <code>base</code> (and thus <code>base</code> is fully merged + * into <code>tip</code>); false otherwise. + * @throws MissingObjectException + * one or or more of the next commit's parents are not available + * from the object database, but were thought to be candidates + * for traversal. This usually indicates a broken link. + * @throws IncorrectObjectTypeException + * one or or more of the next commit's parents are not actually + * commit objects. + * @throws IOException + * a pack file or loose object could not be read. + */ + public boolean isMergedInto(final RevCommit base, final RevCommit tip) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + final RevFilter oldRF = filter; + final TreeFilter oldTF = treeFilter; + try { + finishDelayedFreeFlags(); + reset(~freeFlags & APP_FLAGS); + filter = RevFilter.MERGE_BASE; + treeFilter = TreeFilter.ALL; + markStart(tip); + markStart(base); + return next() == base; + } finally { + filter = oldRF; + treeFilter = oldTF; + } + } + + /** + * Pop the next most recent commit. + * + * @return next most recent commit; null if traversal is over. + * @throws MissingObjectException + * one or or more of the next commit's parents are not available + * from the object database, but were thought to be candidates + * for traversal. This usually indicates a broken link. + * @throws IncorrectObjectTypeException + * one or or more of the next commit's parents are not actually + * commit objects. + * @throws IOException + * a pack file or loose object could not be read. + */ + public RevCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + return pending.next(); + } + + /** + * Obtain the sort types applied to the commits returned. + * + * @return the sorting strategies employed. At least one strategy is always + * used, but that strategy may be {@link RevSort#NONE}. + */ + public EnumSet<RevSort> getRevSort() { + return sorting.clone(); + } + + /** + * Check whether the provided sorting strategy is enabled. + * + * @param sort + * a sorting strategy to look for. + * @return true if this strategy is enabled, false otherwise + */ + public boolean hasRevSort(RevSort sort) { + return sorting.contains(sort); + } + + /** + * Select a single sorting strategy for the returned commits. + * <p> + * Disables all sorting strategies, then enables only the single strategy + * supplied by the caller. + * + * @param s + * a sorting strategy to enable. + */ + public void sort(final RevSort s) { + assertNotStarted(); + sorting.clear(); + sorting.add(s); + } + + /** + * Add or remove a sorting strategy for the returned commits. + * <p> + * Multiple strategies can be applied at once, in which case some strategies + * may take precedence over others. As an example, {@link RevSort#TOPO} must + * take precedence over {@link RevSort#COMMIT_TIME_DESC}, otherwise it + * cannot enforce its ordering. + * + * @param s + * a sorting strategy to enable or disable. + * @param use + * true if this strategy should be used, false if it should be + * removed. + */ + public void sort(final RevSort s, final boolean use) { + assertNotStarted(); + if (use) + sorting.add(s); + else + sorting.remove(s); + + if (sorting.size() > 1) + sorting.remove(RevSort.NONE); + else if (sorting.size() == 0) + sorting.add(RevSort.NONE); + } + + /** + * Get the currently configured commit filter. + * + * @return the current filter. Never null as a filter is always needed. + */ + public RevFilter getRevFilter() { + return filter; + } + + /** + * Set the commit filter for this walker. + * <p> + * Multiple filters may be combined by constructing an arbitrary tree of + * <code>AndRevFilter</code> or <code>OrRevFilter</code> instances to + * describe the boolean expression required by the application. Custom + * filter implementations may also be constructed by applications. + * <p> + * Note that filters are not thread-safe and may not be shared by concurrent + * RevWalk instances. Every RevWalk must be supplied its own unique filter, + * unless the filter implementation specifically states it is (and always + * will be) thread-safe. Callers may use {@link RevFilter#clone()} to create + * a unique filter tree for this RevWalk instance. + * + * @param newFilter + * the new filter. If null the special {@link RevFilter#ALL} + * filter will be used instead, as it matches every commit. + * @see org.eclipse.jgit.revwalk.filter.AndRevFilter + * @see org.eclipse.jgit.revwalk.filter.OrRevFilter + */ + public void setRevFilter(final RevFilter newFilter) { + assertNotStarted(); + filter = newFilter != null ? newFilter : RevFilter.ALL; + } + + /** + * Get the tree filter used to simplify commits by modified paths. + * + * @return the current filter. Never null as a filter is always needed. If + * no filter is being applied {@link TreeFilter#ALL} is returned. + */ + public TreeFilter getTreeFilter() { + return treeFilter; + } + + /** + * Set the tree filter used to simplify commits by modified paths. + * <p> + * If null or {@link TreeFilter#ALL} the path limiter is removed. Commits + * will not be simplified. + * <p> + * If non-null and not {@link TreeFilter#ALL} then the tree filter will be + * installed and commits will have their ancestry simplified to hide commits + * that do not contain tree entries matched by the filter. + * <p> + * Usually callers should be inserting a filter graph including + * {@link TreeFilter#ANY_DIFF} along with one or more + * {@link org.eclipse.jgit.treewalk.filter.PathFilter} instances. + * + * @param newFilter + * new filter. If null the special {@link TreeFilter#ALL} filter + * will be used instead, as it matches everything. + * @see org.eclipse.jgit.treewalk.filter.PathFilter + */ + public void setTreeFilter(final TreeFilter newFilter) { + assertNotStarted(); + treeFilter = newFilter != null ? newFilter : TreeFilter.ALL; + } + + /** + * Should the body of a commit or tag be retained after parsing its headers? + * <p> + * Usually the body is always retained, but some application code might not + * care and would prefer to discard the body of a commit as early as + * possible, to reduce memory usage. + * + * @return true if the body should be retained; false it is discarded. + */ + public boolean isRetainBody() { + return retainBody; + } + + /** + * Set whether or not the body of a commit or tag is retained. + * <p> + * If a body of a commit or tag is not retained, the application must + * call {@link #parseBody(RevObject)} before the body can be safely + * accessed through the type specific access methods. + * + * @param retain true to retain bodies; false to discard them early. + */ + public void setRetainBody(final boolean retain) { + retainBody = retain; + } + + /** + * Locate a reference to a blob without loading it. + * <p> + * The blob may or may not exist in the repository. It is impossible to tell + * from this method's return value. + * + * @param id + * name of the blob object. + * @return reference to the blob object. Never null. + */ + public RevBlob lookupBlob(final AnyObjectId id) { + RevBlob c = (RevBlob) objects.get(id); + if (c == null) { + c = new RevBlob(id); + objects.add(c); + } + return c; + } + + /** + * Locate a reference to a tree without loading it. + * <p> + * The tree may or may not exist in the repository. It is impossible to tell + * from this method's return value. + * + * @param id + * name of the tree object. + * @return reference to the tree object. Never null. + */ + public RevTree lookupTree(final AnyObjectId id) { + RevTree c = (RevTree) objects.get(id); + if (c == null) { + c = new RevTree(id); + objects.add(c); + } + return c; + } + + /** + * Locate a reference to a commit without loading it. + * <p> + * The commit may or may not exist in the repository. It is impossible to + * tell from this method's return value. + * + * @param id + * name of the commit object. + * @return reference to the commit object. Never null. + */ + public RevCommit lookupCommit(final AnyObjectId id) { + RevCommit c = (RevCommit) objects.get(id); + if (c == null) { + c = createCommit(id); + objects.add(c); + } + return c; + } + + /** + * Locate a reference to any object without loading it. + * <p> + * The object may or may not exist in the repository. It is impossible to + * tell from this method's return value. + * + * @param id + * name of the object. + * @param type + * type of the object. Must be a valid Git object type. + * @return reference to the object. Never null. + */ + public RevObject lookupAny(final AnyObjectId id, final int type) { + RevObject r = objects.get(id); + if (r == null) { + switch (type) { + case Constants.OBJ_COMMIT: + r = createCommit(id); + break; + case Constants.OBJ_TREE: + r = new RevTree(id); + break; + case Constants.OBJ_BLOB: + r = new RevBlob(id); + break; + case Constants.OBJ_TAG: + r = new RevTag(id); + break; + default: + throw new IllegalArgumentException("invalid git type: " + type); + } + objects.add(r); + } + return r; + } + + /** + * Locate a reference to a commit and immediately parse its content. + * <p> + * Unlike {@link #lookupCommit(AnyObjectId)} this method only returns + * successfully if the commit object exists, is verified to be a commit, and + * was parsed without error. + * + * @param id + * name of the commit object. + * @return reference to the commit object. Never null. + * @throws MissingObjectException + * the supplied commit does not exist. + * @throws IncorrectObjectTypeException + * the supplied id is not a commit or an annotated tag. + * @throws IOException + * a pack file or loose object could not be read. + */ + public RevCommit parseCommit(final AnyObjectId id) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + RevObject c = parseAny(id); + while (c instanceof RevTag) { + c = ((RevTag) c).getObject(); + parseHeaders(c); + } + if (!(c instanceof RevCommit)) + throw new IncorrectObjectTypeException(id.toObjectId(), + Constants.TYPE_COMMIT); + return (RevCommit) c; + } + + /** + * Locate a reference to a tree. + * <p> + * This method only returns successfully if the tree object exists, is + * verified to be a tree. + * + * @param id + * name of the tree object, or a commit or annotated tag that may + * reference a tree. + * @return reference to the tree object. Never null. + * @throws MissingObjectException + * the supplied tree does not exist. + * @throws IncorrectObjectTypeException + * the supplied id is not a tree, a commit or an annotated tag. + * @throws IOException + * a pack file or loose object could not be read. + */ + public RevTree parseTree(final AnyObjectId id) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + RevObject c = parseAny(id); + while (c instanceof RevTag) { + c = ((RevTag) c).getObject(); + parseHeaders(c); + } + + final RevTree t; + if (c instanceof RevCommit) + t = ((RevCommit) c).getTree(); + else if (!(c instanceof RevTree)) + throw new IncorrectObjectTypeException(id.toObjectId(), + Constants.TYPE_TREE); + else + t = (RevTree) c; + parseHeaders(t); + return t; + } + + /** + * Locate a reference to any object and immediately parse its headers. + * <p> + * This method only returns successfully if the object exists and was parsed + * without error. Parsing an object can be expensive as the type must be + * determined. For blobs this may mean the blob content was unpacked + * unnecessarily, and thrown away. + * + * @param id + * name of the object. + * @return reference to the object. Never null. + * @throws MissingObjectException + * the supplied does not exist. + * @throws IOException + * a pack file or loose object could not be read. + */ + public RevObject parseAny(final AnyObjectId id) + throws MissingObjectException, IOException { + RevObject r = objects.get(id); + if (r == null) { + final ObjectLoader ldr = db.openObject(curs, id); + if (ldr == null) + throw new MissingObjectException(id.toObjectId(), "unknown"); + final byte[] data = ldr.getCachedBytes(); + final int type = ldr.getType(); + switch (type) { + case Constants.OBJ_COMMIT: { + final RevCommit c = createCommit(id); + c.parseCanonical(this, data); + r = c; + break; + } + case Constants.OBJ_TREE: { + r = new RevTree(id); + r.flags |= PARSED; + break; + } + case Constants.OBJ_BLOB: { + r = new RevBlob(id); + r.flags |= PARSED; + break; + } + case Constants.OBJ_TAG: { + final RevTag t = new RevTag(id); + t.parseCanonical(this, data); + r = t; + break; + } + default: + throw new IllegalArgumentException("Bad object type: " + type); + } + objects.add(r); + } else + parseHeaders(r); + return r; + } + + /** + * Ensure the object's critical headers have been parsed. + * <p> + * This method only returns successfully if the object exists and was parsed + * without error. + * + * @param obj + * the object the caller needs to be parsed. + * @throws MissingObjectException + * the supplied does not exist. + * @throws IOException + * a pack file or loose object could not be read. + */ + public void parseHeaders(final RevObject obj) + throws MissingObjectException, IOException { + if ((obj.flags & PARSED) == 0) + obj.parseHeaders(this); + } + + /** + * Ensure the object's fully body content is available. + * <p> + * This method only returns successfully if the object exists and was parsed + * without error. + * + * @param obj + * the object the caller needs to be parsed. + * @throws MissingObjectException + * the supplied does not exist. + * @throws IOException + * a pack file or loose object could not be read. + */ + public void parseBody(final RevObject obj) + throws MissingObjectException, IOException { + obj.parseBody(this); + } + + /** + * Create a new flag for application use during walking. + * <p> + * Applications are only assured to be able to create 24 unique flags on any + * given revision walker instance. Any flags beyond 24 are offered only if + * the implementation has extra free space within its internal storage. + * + * @param name + * description of the flag, primarily useful for debugging. + * @return newly constructed flag instance. + * @throws IllegalArgumentException + * too many flags have been reserved on this revision walker. + */ + public RevFlag newFlag(final String name) { + final int m = allocFlag(); + return new RevFlag(this, name, m); + } + + int allocFlag() { + if (freeFlags == 0) + throw new IllegalArgumentException(32 - RESERVED_FLAGS + + " flags already created."); + final int m = Integer.lowestOneBit(freeFlags); + freeFlags &= ~m; + return m; + } + + /** + * Automatically carry a flag from a child commit to its parents. + * <p> + * A carried flag is copied from the child commit onto its parents when the + * child commit is popped from the lowest level of walk's internal graph. + * + * @param flag + * the flag to carry onto parents, if set on a descendant. + */ + public void carry(final RevFlag flag) { + if ((freeFlags & flag.mask) != 0) + throw new IllegalArgumentException(flag.name + " is disposed."); + if (flag.walker != this) + throw new IllegalArgumentException(flag.name + " not from this."); + carryFlags |= flag.mask; + } + + /** + * Automatically carry flags from a child commit to its parents. + * <p> + * A carried flag is copied from the child commit onto its parents when the + * child commit is popped from the lowest level of walk's internal graph. + * + * @param set + * the flags to carry onto parents, if set on a descendant. + */ + public void carry(final Collection<RevFlag> set) { + for (final RevFlag flag : set) + carry(flag); + } + + /** + * Allow a flag to be recycled for a different use. + * <p> + * Recycled flags always come back as a different Java object instance when + * assigned again by {@link #newFlag(String)}. + * <p> + * If the flag was previously being carried, the carrying request is + * removed. Disposing of a carried flag while a traversal is in progress has + * an undefined behavior. + * + * @param flag + * the to recycle. + */ + public void disposeFlag(final RevFlag flag) { + freeFlag(flag.mask); + } + + void freeFlag(final int mask) { + if (isNotStarted()) { + freeFlags |= mask; + carryFlags &= ~mask; + } else { + delayFreeFlags |= mask; + } + } + + private void finishDelayedFreeFlags() { + if (delayFreeFlags != 0) { + freeFlags |= delayFreeFlags; + carryFlags &= ~delayFreeFlags; + delayFreeFlags = 0; + } + } + + /** + * Resets internal state and allows this instance to be used again. + * <p> + * Unlike {@link #dispose()} previously acquired RevObject (and RevCommit) + * instances are not invalidated. RevFlag instances are not invalidated, but + * are removed from all RevObjects. + */ + public final void reset() { + reset(0); + } + + /** + * Resets internal state and allows this instance to be used again. + * <p> + * Unlike {@link #dispose()} previously acquired RevObject (and RevCommit) + * instances are not invalidated. RevFlag instances are not invalidated, but + * are removed from all RevObjects. + * + * @param retainFlags + * application flags that should <b>not</b> be cleared from + * existing commit objects. + */ + public final void resetRetain(final RevFlagSet retainFlags) { + reset(retainFlags.mask); + } + + /** + * Resets internal state and allows this instance to be used again. + * <p> + * Unlike {@link #dispose()} previously acquired RevObject (and RevCommit) + * instances are not invalidated. RevFlag instances are not invalidated, but + * are removed from all RevObjects. + * + * @param retainFlags + * application flags that should <b>not</b> be cleared from + * existing commit objects. + */ + public final void resetRetain(final RevFlag... retainFlags) { + int mask = 0; + for (final RevFlag flag : retainFlags) + mask |= flag.mask; + reset(mask); + } + + /** + * Resets internal state and allows this instance to be used again. + * <p> + * Unlike {@link #dispose()} previously acquired RevObject (and RevCommit) + * instances are not invalidated. RevFlag instances are not invalidated, but + * are removed from all RevObjects. + * + * @param retainFlags + * application flags that should <b>not</b> be cleared from + * existing commit objects. + */ + protected void reset(int retainFlags) { + finishDelayedFreeFlags(); + retainFlags |= PARSED; + final int clearFlags = ~retainFlags; + + final FIFORevQueue q = new FIFORevQueue(); + for (final RevCommit c : roots) { + if ((c.flags & clearFlags) == 0) + continue; + c.flags &= retainFlags; + c.reset(); + q.add(c); + } + + for (;;) { + final RevCommit c = q.next(); + if (c == null) + break; + if (c.parents == null) + continue; + for (final RevCommit p : c.parents) { + if ((p.flags & clearFlags) == 0) + continue; + p.flags &= retainFlags; + p.reset(); + q.add(p); + } + } + + curs.release(); + roots.clear(); + queue = new DateRevQueue(); + pending = new StartGenerator(this); + } + + /** + * Dispose all internal state and invalidate all RevObject instances. + * <p> + * All RevObject (and thus RevCommit, etc.) instances previously acquired + * from this RevWalk are invalidated by a dispose call. Applications must + * not retain or use RevObject instances obtained prior to the dispose call. + * All RevFlag instances are also invalidated, and must not be reused. + */ + public void dispose() { + freeFlags = APP_FLAGS; + delayFreeFlags = 0; + carryFlags = UNINTERESTING; + objects.clear(); + curs.release(); + roots.clear(); + queue = new DateRevQueue(); + pending = new StartGenerator(this); + } + + /** + * Returns an Iterator over the commits of this walker. + * <p> + * The returned iterator is only useful for one walk. If this RevWalk gets + * reset a new iterator must be obtained to walk over the new results. + * <p> + * Applications must not use both the Iterator and the {@link #next()} API + * at the same time. Pick one API and use that for the entire walk. + * <p> + * If a checked exception is thrown during the walk (see {@link #next()}) + * it is rethrown from the Iterator as a {@link RevWalkException}. + * + * @return an iterator over this walker's commits. + * @see RevWalkException + */ + public Iterator<RevCommit> iterator() { + final RevCommit first; + try { + first = RevWalk.this.next(); + } catch (MissingObjectException e) { + throw new RevWalkException(e); + } catch (IncorrectObjectTypeException e) { + throw new RevWalkException(e); + } catch (IOException e) { + throw new RevWalkException(e); + } + + return new Iterator<RevCommit>() { + RevCommit next = first; + + public boolean hasNext() { + return next != null; + } + + public RevCommit next() { + try { + final RevCommit r = next; + next = RevWalk.this.next(); + return r; + } catch (MissingObjectException e) { + throw new RevWalkException(e); + } catch (IncorrectObjectTypeException e) { + throw new RevWalkException(e); + } catch (IOException e) { + throw new RevWalkException(e); + } + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + /** Throws an exception if we have started producing output. */ + protected void assertNotStarted() { + if (isNotStarted()) + return; + throw new IllegalStateException("Output has already been started."); + } + + private boolean isNotStarted() { + return pending instanceof StartGenerator; + } + + /** + * Construct a new unparsed commit for the given object. + * + * @param id + * the object this walker requires a commit reference for. + * @return a new unparsed reference for the object. + */ + protected RevCommit createCommit(final AnyObjectId id) { + return new RevCommit(id); + } + + void carryFlagsImpl(final RevCommit c) { + final int carry = c.flags & carryFlags; + if (carry != 0) + RevCommit.carryFlags(c, carry); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java new file mode 100644 index 0000000000..04d8def436 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; + +/** + * Replaces a RevCommit's parents until not colored with REWRITE. + * <p> + * Before a RevCommit is returned to the caller its parents are updated to + * create a dense DAG. Instead of reporting the actual parents as recorded when + * the commit was created the returned commit will reflect the next closest + * commit that matched the revision walker's filters. + * <p> + * This generator is the second phase of a path limited revision walk and + * assumes it is receiving RevCommits from {@link RewriteTreeFilter}, + * after they have been fully buffered by {@link AbstractRevQueue}. The full + * buffering is necessary to allow the simple loop used within our own + * {@link #rewrite(RevCommit)} to pull completely through a strand of + * {@link RevWalk#REWRITE} colored commits and come up with a simplification + * that makes the DAG dense. Not fully buffering the commits first would cause + * this loop to abort early, due to commits not being parsed and colored + * correctly. + * + * @see RewriteTreeFilter + */ +class RewriteGenerator extends Generator { + private static final int REWRITE = RevWalk.REWRITE; + + /** For {@link #cleanup(RevCommit[])} to remove duplicate parents. */ + private static final int DUPLICATE = RevWalk.TEMP_MARK; + + private final Generator source; + + RewriteGenerator(final Generator s) { + source = s; + } + + @Override + void shareFreeList(final BlockRevQueue q) { + source.shareFreeList(q); + } + + @Override + int outputType() { + return source.outputType() & ~NEEDS_REWRITE; + } + + @Override + RevCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + for (;;) { + final RevCommit c = source.next(); + if (c == null) + return null; + + boolean rewrote = false; + final RevCommit[] pList = c.parents; + final int nParents = pList.length; + for (int i = 0; i < nParents; i++) { + final RevCommit oldp = pList[i]; + final RevCommit newp = rewrite(oldp); + if (oldp != newp) { + pList[i] = newp; + rewrote = true; + } + } + if (rewrote) + c.parents = cleanup(pList); + + return c; + } + } + + private RevCommit rewrite(RevCommit p) { + for (;;) { + final RevCommit[] pList = p.parents; + if (pList.length > 1) { + // This parent is a merge, so keep it. + // + return p; + } + + if ((p.flags & RevWalk.UNINTERESTING) != 0) { + // Retain uninteresting parents. They show where the + // DAG was cut off because it wasn't interesting. + // + return p; + } + + if ((p.flags & REWRITE) == 0) { + // This parent was not eligible for rewriting. We + // need to keep it in the DAG. + // + return p; + } + + if (pList.length == 0) { + // We can't go back any further, other than to + // just delete the parent entirely. + // + return null; + } + + p = pList[0]; + } + } + + private RevCommit[] cleanup(final RevCommit[] oldList) { + // Remove any duplicate parents caused due to rewrites (e.g. a merge + // with two sides that both simplified back into the merge base). + // We also may have deleted a parent by marking it null. + // + int newCnt = 0; + for (int o = 0; o < oldList.length; o++) { + final RevCommit p = oldList[o]; + if (p == null) + continue; + if ((p.flags & DUPLICATE) != 0) { + oldList[o] = null; + continue; + } + p.flags |= DUPLICATE; + newCnt++; + } + + if (newCnt == oldList.length) { + for (final RevCommit p : oldList) + p.flags &= ~DUPLICATE; + return oldList; + } + + final RevCommit[] newList = new RevCommit[newCnt]; + newCnt = 0; + for (final RevCommit p : oldList) { + if (p != null) { + newList[newCnt++] = p; + p.flags &= ~DUPLICATE; + } + } + + return newList; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteTreeFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteTreeFilter.java new file mode 100644 index 0000000000..4cec8a7f0f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteTreeFilter.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.StopWalkException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.filter.RevFilter; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.TreeFilter; + +/** + * First phase of a path limited revision walk. + * <p> + * This filter is ANDed to evaluate after all other filters and ties the + * configured {@link TreeFilter} into the revision walking process. + * <p> + * Each commit is differenced concurrently against all of its parents to look + * for tree entries that are interesting to the TreeFilter. If none are found + * the commit is colored with {@link RevWalk#REWRITE}, allowing a later pass + * implemented by {@link RewriteGenerator} to remove those colored commits from + * the DAG. + * + * @see RewriteGenerator + */ +class RewriteTreeFilter extends RevFilter { + private static final int PARSED = RevWalk.PARSED; + + private static final int UNINTERESTING = RevWalk.UNINTERESTING; + + private static final int REWRITE = RevWalk.REWRITE; + + private final TreeWalk pathFilter; + + RewriteTreeFilter(final RevWalk walker, final TreeFilter t) { + pathFilter = new TreeWalk(walker.db); + pathFilter.setFilter(t); + pathFilter.setRecursive(t.shouldBeRecursive()); + } + + @Override + public RevFilter clone() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean include(final RevWalk walker, final RevCommit c) + throws StopWalkException, MissingObjectException, + IncorrectObjectTypeException, IOException { + // Reset the tree filter to scan this commit and parents. + // + final RevCommit[] pList = c.parents; + final int nParents = pList.length; + final TreeWalk tw = pathFilter; + final ObjectId[] trees = new ObjectId[nParents + 1]; + for (int i = 0; i < nParents; i++) { + final RevCommit p = c.parents[i]; + if ((p.flags & PARSED) == 0) + p.parseHeaders(walker); + trees[i] = p.getTree(); + } + trees[nParents] = c.getTree(); + tw.reset(trees); + + if (nParents == 1) { + // We have exactly one parent. This is a very common case. + // + int chgs = 0, adds = 0; + while (tw.next()) { + chgs++; + if (tw.getRawMode(0) == 0 && tw.getRawMode(1) != 0) + adds++; + else + break; // no point in looking at this further. + } + + if (chgs == 0) { + // No changes, so our tree is effectively the same as + // our parent tree. We pass the buck to our parent. + // + c.flags |= REWRITE; + return false; + } else { + // We have interesting items, but neither of the special + // cases denoted above. + // + return true; + } + } else if (nParents == 0) { + // We have no parents to compare against. Consider us to be + // REWRITE only if we have no paths matching our filter. + // + if (tw.next()) + return true; + c.flags |= REWRITE; + return false; + } + + // We are a merge commit. We can only be REWRITE if we are same + // to _all_ parents. We may also be able to eliminate a parent if + // it does not contribute changes to us. Such a parent may be an + // uninteresting side branch. + // + final int[] chgs = new int[nParents]; + final int[] adds = new int[nParents]; + while (tw.next()) { + final int myMode = tw.getRawMode(nParents); + for (int i = 0; i < nParents; i++) { + final int pMode = tw.getRawMode(i); + if (myMode == pMode && tw.idEqual(i, nParents)) + continue; + + chgs[i]++; + if (pMode == 0 && myMode != 0) + adds[i]++; + } + } + + boolean same = false; + boolean diff = false; + for (int i = 0; i < nParents; i++) { + if (chgs[i] == 0) { + // No changes, so our tree is effectively the same as + // this parent tree. We pass the buck to only this one + // parent commit. + // + + final RevCommit p = pList[i]; + if ((p.flags & UNINTERESTING) != 0) { + // This parent was marked as not interesting by the + // application. We should look for another parent + // that is interesting. + // + same = true; + continue; + } + + c.flags |= REWRITE; + c.parents = new RevCommit[] { p }; + return false; + } + + if (chgs[i] == adds[i]) { + // All of the differences from this parent were because we + // added files that they did not have. This parent is our + // "empty tree root" and thus their history is not relevant. + // Cut our grandparents to be an empty list. + // + pList[i].parents = RevCommit.NO_PARENTS; + } + + // We have an interesting difference relative to this parent. + // + diff = true; + } + + if (diff && !same) { + // We did not abort above, so we are different in at least one + // way from all of our parents. We have to take the blame for + // that difference. + // + return true; + } + + // We are the same as all of our parents. We must keep them + // as they are and allow those parents to flow into pending + // for further scanning. + // + c.flags |= REWRITE; + return false; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java new file mode 100644 index 0000000000..c5353fe8c5 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.revwalk.filter.AndRevFilter; +import org.eclipse.jgit.revwalk.filter.RevFilter; +import org.eclipse.jgit.treewalk.filter.TreeFilter; + +/** + * Initial RevWalk generator that bootstraps a new walk. + * <p> + * Initially RevWalk starts with this generator as its chosen implementation. + * The first request for a RevCommit from the RevWalk instance calls to our + * {@link #next()} method, and we replace ourselves with the best Generator + * implementation available based upon the current RevWalk configuration. + */ +class StartGenerator extends Generator { + private final RevWalk walker; + + StartGenerator(final RevWalk w) { + walker = w; + } + + @Override + int outputType() { + return 0; + } + + @Override + RevCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + Generator g; + + final RevWalk w = walker; + RevFilter rf = w.getRevFilter(); + final TreeFilter tf = w.getTreeFilter(); + AbstractRevQueue q = walker.queue; + + if (rf == RevFilter.MERGE_BASE) { + // Computing for merge bases is a special case and does not + // use the bulk of the generator pipeline. + // + if (tf != TreeFilter.ALL) + throw new IllegalStateException("Cannot combine TreeFilter " + + tf + " with RevFilter " + rf + "."); + + final MergeBaseGenerator mbg = new MergeBaseGenerator(w); + walker.pending = mbg; + walker.queue = AbstractRevQueue.EMPTY_QUEUE; + mbg.init(q); + return mbg.next(); + } + + final boolean uninteresting = q.anybodyHasFlag(RevWalk.UNINTERESTING); + boolean boundary = walker.hasRevSort(RevSort.BOUNDARY); + + if (!boundary && walker instanceof ObjectWalk) { + // The object walker requires boundary support to color + // trees and blobs at the boundary uninteresting so it + // does not produce those in the result. + // + boundary = true; + } + if (boundary && !uninteresting) { + // If we were not fed uninteresting commits we will never + // construct a boundary. There is no reason to include the + // extra overhead associated with that in our pipeline. + // + boundary = false; + } + + final DateRevQueue pending; + int pendingOutputType = 0; + if (q instanceof DateRevQueue) + pending = (DateRevQueue)q; + else + pending = new DateRevQueue(q); + if (tf != TreeFilter.ALL) { + rf = AndRevFilter.create(rf, new RewriteTreeFilter(w, tf)); + pendingOutputType |= HAS_REWRITE | NEEDS_REWRITE; + } + + walker.queue = q; + g = new PendingGenerator(w, pending, rf, pendingOutputType); + + if (boundary) { + // Because the boundary generator may produce uninteresting + // commits we cannot allow the pending generator to dispose + // of them early. + // + ((PendingGenerator) g).canDispose = false; + } + + if ((g.outputType() & NEEDS_REWRITE) != 0) { + // Correction for an upstream NEEDS_REWRITE is to buffer + // fully and then apply a rewrite generator that can + // pull through the rewrite chain and produce a dense + // output graph. + // + g = new FIFORevQueue(g); + g = new RewriteGenerator(g); + } + + if (walker.hasRevSort(RevSort.TOPO) + && (g.outputType() & SORT_TOPO) == 0) + g = new TopoSortGenerator(g); + if (walker.hasRevSort(RevSort.REVERSE)) + g = new LIFORevQueue(g); + if (boundary) + g = new BoundaryGenerator(w, g); + else if (uninteresting) { + // Try to protect ourselves from uninteresting commits producing + // due to clock skew in the commit time stamps. Delay such that + // we have a chance at coloring enough of the graph correctly, + // and then strip any UNINTERESTING nodes that may have leaked + // through early. + // + if (pending.peek() != null) + g = new DelayRevQueue(g); + g = new FixUninterestingGenerator(g); + } + + w.pending = g; + return g.next(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java new file mode 100644 index 0000000000..78c0d8f16a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; + +/** Sorts commits in topological order. */ +class TopoSortGenerator extends Generator { + private static final int TOPO_DELAY = RevWalk.TOPO_DELAY; + + private final FIFORevQueue pending; + + private final int outputType; + + /** + * Create a new sorter and completely spin the generator. + * <p> + * When the constructor completes the supplied generator will have no + * commits remaining, as all of the commits will be held inside of this + * generator's internal buffer. + * + * @param s + * generator to pull all commits out of, and into this buffer. + * @throws MissingObjectException + * @throws IncorrectObjectTypeException + * @throws IOException + */ + TopoSortGenerator(final Generator s) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + pending = new FIFORevQueue(); + outputType = s.outputType() | SORT_TOPO; + s.shareFreeList(pending); + for (;;) { + final RevCommit c = s.next(); + if (c == null) + break; + for (final RevCommit p : c.parents) + p.inDegree++; + pending.add(c); + } + } + + @Override + int outputType() { + return outputType; + } + + @Override + void shareFreeList(final BlockRevQueue q) { + q.shareFreeList(pending); + } + + @Override + RevCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + for (;;) { + final RevCommit c = pending.next(); + if (c == null) + return null; + + if (c.inDegree > 0) { + // At least one of our children is missing. We delay + // production until all of our children are output. + // + c.flags |= TOPO_DELAY; + continue; + } + + // All of our children have already produced, + // so it is OK for us to produce now as well. + // + for (final RevCommit p : c.parents) { + if (--p.inDegree == 0 && (p.flags & TOPO_DELAY) != 0) { + // This parent tried to come before us, but we are + // his last child. unpop the parent so it goes right + // behind this child. + // + p.flags &= ~TOPO_DELAY; + pending.unpop(p); + } + } + return c; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/AndRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/AndRevFilter.java new file mode 100644 index 0000000000..406a7764d5 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/AndRevFilter.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk.filter; + +import java.io.IOException; +import java.util.Collection; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Includes a commit only if all subfilters include the same commit. + * <p> + * Classic shortcut behavior is used, so evaluation of the + * {@link RevFilter#include(RevWalk, RevCommit)} method stops as soon as a false + * result is obtained. Applications can improve filtering performance by placing + * faster filters that are more likely to reject a result earlier in the list. + */ +public abstract class AndRevFilter extends RevFilter { + /** + * Create a filter with two filters, both of which must match. + * + * @param a + * first filter to test. + * @param b + * second filter to test. + * @return a filter that must match both input filters. + */ + public static RevFilter create(final RevFilter a, final RevFilter b) { + if (a == ALL) + return b; + if (b == ALL) + return a; + return new Binary(a, b); + } + + /** + * Create a filter around many filters, all of which must match. + * + * @param list + * list of filters to match against. Must contain at least 2 + * filters. + * @return a filter that must match all input filters. + */ + public static RevFilter create(final RevFilter[] list) { + if (list.length == 2) + return create(list[0], list[1]); + if (list.length < 2) + throw new IllegalArgumentException("At least two filters needed."); + final RevFilter[] subfilters = new RevFilter[list.length]; + System.arraycopy(list, 0, subfilters, 0, list.length); + return new List(subfilters); + } + + /** + * Create a filter around many filters, all of which must match. + * + * @param list + * list of filters to match against. Must contain at least 2 + * filters. + * @return a filter that must match all input filters. + */ + public static RevFilter create(final Collection<RevFilter> list) { + if (list.size() < 2) + throw new IllegalArgumentException("At least two filters needed."); + final RevFilter[] subfilters = new RevFilter[list.size()]; + list.toArray(subfilters); + if (subfilters.length == 2) + return create(subfilters[0], subfilters[1]); + return new List(subfilters); + } + + private static class Binary extends AndRevFilter { + private final RevFilter a; + + private final RevFilter b; + + Binary(final RevFilter one, final RevFilter two) { + a = one; + b = two; + } + + @Override + public boolean include(final RevWalk walker, final RevCommit c) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + return a.include(walker, c) && b.include(walker, c); + } + + @Override + public RevFilter clone() { + return new Binary(a.clone(), b.clone()); + } + + @Override + public String toString() { + return "(" + a.toString() + " AND " + b.toString() + ")"; + } + } + + private static class List extends AndRevFilter { + private final RevFilter[] subfilters; + + List(final RevFilter[] list) { + subfilters = list; + } + + @Override + public boolean include(final RevWalk walker, final RevCommit c) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + for (final RevFilter f : subfilters) { + if (!f.include(walker, c)) + return false; + } + return true; + } + + @Override + public RevFilter clone() { + final RevFilter[] s = new RevFilter[subfilters.length]; + for (int i = 0; i < s.length; i++) + s[i] = subfilters[i].clone(); + return new List(s); + } + + @Override + public String toString() { + final StringBuffer r = new StringBuffer(); + r.append("("); + for (int i = 0; i < subfilters.length; i++) { + if (i > 0) + r.append(" AND "); + r.append(subfilters[i].toString()); + } + r.append(")"); + return r.toString(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/AuthorRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/AuthorRevFilter.java new file mode 100644 index 0000000000..2ede91b57f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/AuthorRevFilter.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk.filter; + +import java.util.regex.Pattern; + +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.util.RawCharSequence; +import org.eclipse.jgit.util.RawParseUtils; + +/** Matches only commits whose author name matches the pattern. */ +public class AuthorRevFilter { + /** + * Create a new author filter. + * <p> + * An optimized substring search may be automatically selected if the + * pattern does not contain any regular expression meta-characters. + * <p> + * The search is performed using a case-insensitive comparison. The + * character encoding of the commit message itself is not respected. The + * filter matches on raw UTF-8 byte sequences. + * + * @param pattern + * regular expression pattern to match. + * @return a new filter that matches the given expression against the author + * name and address of a commit. + */ + public static RevFilter create(String pattern) { + if (pattern.length() == 0) + throw new IllegalArgumentException("Cannot match on empty string."); + if (SubStringRevFilter.safe(pattern)) + return new SubStringSearch(pattern); + return new PatternSearch(pattern); + } + + private AuthorRevFilter() { + // Don't permit us to be created. + } + + static RawCharSequence textFor(final RevCommit cmit) { + final byte[] raw = cmit.getRawBuffer(); + final int b = RawParseUtils.author(raw, 0); + if (b < 0) + return RawCharSequence.EMPTY; + final int e = RawParseUtils.nextLF(raw, b, '>'); + return new RawCharSequence(raw, b, e); + } + + private static class PatternSearch extends PatternMatchRevFilter { + PatternSearch(final String patternText) { + super(patternText, true, true, Pattern.CASE_INSENSITIVE); + } + + @Override + protected CharSequence text(final RevCommit cmit) { + return textFor(cmit); + } + + @Override + public RevFilter clone() { + return new PatternSearch(pattern()); + } + } + + private static class SubStringSearch extends SubStringRevFilter { + SubStringSearch(final String patternText) { + super(patternText); + } + + @Override + protected RawCharSequence text(final RevCommit cmit) { + return textFor(cmit); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/CommitTimeRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/CommitTimeRevFilter.java new file mode 100644 index 0000000000..a70ff9fd6e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/CommitTimeRevFilter.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2009, Mark Struberg <struberg@yahoo.de> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk.filter; + +import java.io.IOException; +import java.util.Date; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.StopWalkException; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +/** Selects commits based upon the commit time field. */ +public abstract class CommitTimeRevFilter extends RevFilter { + /** + * Create a new filter to select commits before a given date/time. + * + * @param ts + * the point in time to cut on. + * @return a new filter to select commits on or before <code>ts</code>. + */ + public static final RevFilter before(final Date ts) { + return new Before(ts.getTime()); + } + + /** + * Create a new filter to select commits after a given date/time. + * + * @param ts + * the point in time to cut on. + * @return a new filter to select commits on or after <code>ts</code>. + */ + public static final RevFilter after(final Date ts) { + return new After(ts.getTime()); + } + + /** + * Create a new filter to select commits after or equal a given date/time <code>since</code> + * and before or equal a given date/time <code>until</code>. + * + * @param since the point in time to cut on. + * @param until the point in time to cut off. + * @return a new filter to select commits between the given date/times. + */ + public static final RevFilter between(final Date since, final Date until) { + return new Between(since.getTime(), until.getTime()); + } + + final int when; + + CommitTimeRevFilter(final long ts) { + when = (int) (ts / 1000); + } + + @Override + public RevFilter clone() { + return this; + } + + private static class Before extends CommitTimeRevFilter { + Before(final long ts) { + super(ts); + } + + @Override + public boolean include(final RevWalk walker, final RevCommit cmit) + throws StopWalkException, MissingObjectException, + IncorrectObjectTypeException, IOException { + return cmit.getCommitTime() <= when; + } + + @Override + public String toString() { + return super.toString() + "(" + new Date(when * 1000L) + ")"; + } + } + + private static class After extends CommitTimeRevFilter { + After(final long ts) { + super(ts); + } + + @Override + public boolean include(final RevWalk walker, final RevCommit cmit) + throws StopWalkException, MissingObjectException, + IncorrectObjectTypeException, IOException { + // Since the walker sorts commits by commit time we can be + // reasonably certain there is nothing remaining worth our + // scanning if this commit is before the point in question. + // + if (cmit.getCommitTime() < when) + throw StopWalkException.INSTANCE; + return true; + } + + @Override + public String toString() { + return super.toString() + "(" + new Date(when * 1000L) + ")"; + } + } + + private static class Between extends CommitTimeRevFilter { + private final int until; + + Between(final long since, final long until) { + super(since); + this.until = (int) (until / 1000); + } + + @Override + public boolean include(final RevWalk walker, final RevCommit cmit) + throws StopWalkException, MissingObjectException, + IncorrectObjectTypeException, IOException { + return cmit.getCommitTime() <= until && cmit.getCommitTime() >= when; + } + + @Override + public String toString() { + return super.toString() + "(" + new Date(when * 1000L) + " - " + new Date(until * 1000L) + ")"; + } + + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/CommitterRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/CommitterRevFilter.java new file mode 100644 index 0000000000..59c3e080d5 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/CommitterRevFilter.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk.filter; + +import java.util.regex.Pattern; + +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.util.RawCharSequence; +import org.eclipse.jgit.util.RawParseUtils; + +/** Matches only commits whose committer name matches the pattern. */ +public class CommitterRevFilter { + /** + * Create a new committer filter. + * <p> + * An optimized substring search may be automatically selected if the + * pattern does not contain any regular expression meta-characters. + * <p> + * The search is performed using a case-insensitive comparison. The + * character encoding of the commit message itself is not respected. The + * filter matches on raw UTF-8 byte sequences. + * + * @param pattern + * regular expression pattern to match. + * @return a new filter that matches the given expression against the author + * name and address of a commit. + */ + public static RevFilter create(String pattern) { + if (pattern.length() == 0) + throw new IllegalArgumentException("Cannot match on empty string."); + if (SubStringRevFilter.safe(pattern)) + return new SubStringSearch(pattern); + return new PatternSearch(pattern); + } + + private CommitterRevFilter() { + // Don't permit us to be created. + } + + static RawCharSequence textFor(final RevCommit cmit) { + final byte[] raw = cmit.getRawBuffer(); + final int b = RawParseUtils.committer(raw, 0); + if (b < 0) + return RawCharSequence.EMPTY; + final int e = RawParseUtils.nextLF(raw, b, '>'); + return new RawCharSequence(raw, b, e); + } + + private static class PatternSearch extends PatternMatchRevFilter { + PatternSearch(final String patternText) { + super(patternText, true, true, Pattern.CASE_INSENSITIVE); + } + + @Override + protected CharSequence text(final RevCommit cmit) { + return textFor(cmit); + } + + @Override + public RevFilter clone() { + return new PatternSearch(pattern()); + } + } + + private static class SubStringSearch extends SubStringRevFilter { + SubStringSearch(final String patternText) { + super(patternText); + } + + @Override + protected RawCharSequence text(final RevCommit cmit) { + return textFor(cmit); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/MessageRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/MessageRevFilter.java new file mode 100644 index 0000000000..6ab3b1d3b0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/MessageRevFilter.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk.filter; + +import java.util.regex.Pattern; + +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.util.RawCharSequence; +import org.eclipse.jgit.util.RawParseUtils; + +/** Matches only commits whose message matches the pattern. */ +public class MessageRevFilter { + /** + * Create a message filter. + * <p> + * An optimized substring search may be automatically selected if the + * pattern does not contain any regular expression meta-characters. + * <p> + * The search is performed using a case-insensitive comparison. The + * character encoding of the commit message itself is not respected. The + * filter matches on raw UTF-8 byte sequences. + * + * @param pattern + * regular expression pattern to match. + * @return a new filter that matches the given expression against the + * message body of the commit. + */ + public static RevFilter create(String pattern) { + if (pattern.length() == 0) + throw new IllegalArgumentException("Cannot match on empty string."); + if (SubStringRevFilter.safe(pattern)) + return new SubStringSearch(pattern); + return new PatternSearch(pattern); + } + + private MessageRevFilter() { + // Don't permit us to be created. + } + + static RawCharSequence textFor(final RevCommit cmit) { + final byte[] raw = cmit.getRawBuffer(); + final int b = RawParseUtils.commitMessage(raw, 0); + if (b < 0) + return RawCharSequence.EMPTY; + return new RawCharSequence(raw, b, raw.length); + } + + private static class PatternSearch extends PatternMatchRevFilter { + PatternSearch(final String patternText) { + super(patternText, true, true, Pattern.CASE_INSENSITIVE + | Pattern.DOTALL); + } + + @Override + protected CharSequence text(final RevCommit cmit) { + return textFor(cmit); + } + + @Override + public RevFilter clone() { + return new PatternSearch(pattern()); + } + } + + private static class SubStringSearch extends SubStringRevFilter { + SubStringSearch(final String patternText) { + super(patternText); + } + + @Override + protected RawCharSequence text(final RevCommit cmit) { + return textFor(cmit); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/NotRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/NotRevFilter.java new file mode 100644 index 0000000000..117378c9fa --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/NotRevFilter.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk.filter; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +/** Includes a commit only if the subfilter does not include the commit. */ +public class NotRevFilter extends RevFilter { + /** + * Create a filter that negates the result of another filter. + * + * @param a + * filter to negate. + * @return a filter that does the reverse of <code>a</code>. + */ + public static RevFilter create(final RevFilter a) { + return new NotRevFilter(a); + } + + private final RevFilter a; + + private NotRevFilter(final RevFilter one) { + a = one; + } + + @Override + public RevFilter negate() { + return a; + } + + @Override + public boolean include(final RevWalk walker, final RevCommit c) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + return !a.include(walker, c); + } + + @Override + public RevFilter clone() { + return new NotRevFilter(a.clone()); + } + + @Override + public String toString() { + return "NOT " + a.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/OrRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/OrRevFilter.java new file mode 100644 index 0000000000..a2782763b6 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/OrRevFilter.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk.filter; + +import java.io.IOException; +import java.util.Collection; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Includes a commit if any subfilters include the same commit. + * <p> + * Classic shortcut behavior is used, so evaluation of the + * {@link RevFilter#include(RevWalk, RevCommit)} method stops as soon as a true + * result is obtained. Applications can improve filtering performance by placing + * faster filters that are more likely to accept a result earlier in the list. + */ +public abstract class OrRevFilter extends RevFilter { + /** + * Create a filter with two filters, one of which must match. + * + * @param a + * first filter to test. + * @param b + * second filter to test. + * @return a filter that must match at least one input filter. + */ + public static RevFilter create(final RevFilter a, final RevFilter b) { + if (a == ALL || b == ALL) + return ALL; + return new Binary(a, b); + } + + /** + * Create a filter around many filters, one of which must match. + * + * @param list + * list of filters to match against. Must contain at least 2 + * filters. + * @return a filter that must match at least one input filter. + */ + public static RevFilter create(final RevFilter[] list) { + if (list.length == 2) + return create(list[0], list[1]); + if (list.length < 2) + throw new IllegalArgumentException("At least two filters needed."); + final RevFilter[] subfilters = new RevFilter[list.length]; + System.arraycopy(list, 0, subfilters, 0, list.length); + return new List(subfilters); + } + + /** + * Create a filter around many filters, one of which must match. + * + * @param list + * list of filters to match against. Must contain at least 2 + * filters. + * @return a filter that must match at least one input filter. + */ + public static RevFilter create(final Collection<RevFilter> list) { + if (list.size() < 2) + throw new IllegalArgumentException("At least two filters needed."); + final RevFilter[] subfilters = new RevFilter[list.size()]; + list.toArray(subfilters); + if (subfilters.length == 2) + return create(subfilters[0], subfilters[1]); + return new List(subfilters); + } + + private static class Binary extends OrRevFilter { + private final RevFilter a; + + private final RevFilter b; + + Binary(final RevFilter one, final RevFilter two) { + a = one; + b = two; + } + + @Override + public boolean include(final RevWalk walker, final RevCommit c) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + return a.include(walker, c) || b.include(walker, c); + } + + @Override + public RevFilter clone() { + return new Binary(a.clone(), b.clone()); + } + + @Override + public String toString() { + return "(" + a.toString() + " OR " + b.toString() + ")"; + } + } + + private static class List extends OrRevFilter { + private final RevFilter[] subfilters; + + List(final RevFilter[] list) { + subfilters = list; + } + + @Override + public boolean include(final RevWalk walker, final RevCommit c) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + for (final RevFilter f : subfilters) { + if (f.include(walker, c)) + return true; + } + return false; + } + + @Override + public RevFilter clone() { + final RevFilter[] s = new RevFilter[subfilters.length]; + for (int i = 0; i < s.length; i++) + s[i] = subfilters[i].clone(); + return new List(s); + } + + @Override + public String toString() { + final StringBuffer r = new StringBuffer(); + r.append("("); + for (int i = 0; i < subfilters.length; i++) { + if (i > 0) + r.append(" OR "); + r.append(subfilters[i].toString()); + } + r.append(")"); + return r.toString(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/PatternMatchRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/PatternMatchRevFilter.java new file mode 100644 index 0000000000..5f2bcf26ab --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/PatternMatchRevFilter.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk.filter; + +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.util.RawCharSequence; + +/** Abstract filter that searches text using extended regular expressions. */ +public abstract class PatternMatchRevFilter extends RevFilter { + /** + * Encode a string pattern for faster matching on byte arrays. + * <p> + * Force the characters to our funny UTF-8 only convention that we use on + * raw buffers. This avoids needing to perform character set decodes on the + * individual commit buffers. + * + * @param patternText + * original pattern string supplied by the user or the + * application. + * @return same pattern, but re-encoded to match our funny raw UTF-8 + * character sequence {@link RawCharSequence}. + */ + protected static final String forceToRaw(final String patternText) { + final byte[] b = Constants.encode(patternText); + final StringBuilder needle = new StringBuilder(b.length); + for (int i = 0; i < b.length; i++) + needle.append((char) (b[i] & 0xff)); + return needle.toString(); + } + + private final String patternText; + + private final Matcher compiledPattern; + + /** + * Construct a new pattern matching filter. + * + * @param pattern + * text of the pattern. Callers may want to surround their + * pattern with ".*" on either end to allow matching in the + * middle of the string. + * @param innerString + * should .* be wrapped around the pattern of ^ and $ are + * missing? Most users will want this set. + * @param rawEncoding + * should {@link #forceToRaw(String)} be applied to the pattern + * before compiling it? + * @param flags + * flags from {@link Pattern} to control how matching performs. + */ + protected PatternMatchRevFilter(String pattern, final boolean innerString, + final boolean rawEncoding, final int flags) { + if (pattern.length() == 0) + throw new IllegalArgumentException("Cannot match on empty string."); + patternText = pattern; + + if (innerString) { + if (!pattern.startsWith("^") && !pattern.startsWith(".*")) + pattern = ".*" + pattern; + if (!pattern.endsWith("$") && !pattern.endsWith(".*")) + pattern = pattern + ".*"; + } + final String p = rawEncoding ? forceToRaw(pattern) : pattern; + compiledPattern = Pattern.compile(p, flags).matcher(""); + } + + /** + * Get the pattern this filter uses. + * + * @return the pattern this filter is applying to candidate strings. + */ + public String pattern() { + return patternText; + } + + @Override + public boolean include(final RevWalk walker, final RevCommit cmit) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + return compiledPattern.reset(text(cmit)).matches(); + } + + /** + * Obtain the raw text to match against. + * + * @param cmit + * current commit being evaluated. + * @return sequence for the commit's content that we need to match on. + */ + protected abstract CharSequence text(RevCommit cmit); + + @Override + public String toString() { + return super.toString() + "(\"" + patternText + "\")"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/RevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/RevFilter.java new file mode 100644 index 0000000000..2d67d9763a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/RevFilter.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk.filter; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.StopWalkException; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Selects interesting revisions during walking. + * <p> + * This is an abstract interface. Applications may implement a subclass, or use + * one of the predefined implementations already available within this package. + * Filters may be chained together using <code>AndRevFilter</code> and + * <code>OrRevFilter</code> to create complex boolean expressions. + * <p> + * Applications should install the filter on a RevWalk by + * {@link RevWalk#setRevFilter(RevFilter)} prior to starting traversal. + * <p> + * Unless specifically noted otherwise a RevFilter implementation is not thread + * safe and may not be shared by different RevWalk instances at the same time. + * This restriction allows RevFilter implementations to cache state within their + * instances during {@link #include(RevWalk, RevCommit)} if it is beneficial to + * their implementation. Deep clones created by {@link #clone()} may be used to + * construct a thread-safe copy of an existing filter. + * + * <p> + * <b>Message filters:</b> + * <ul> + * <li>Author name/email: {@link AuthorRevFilter}</li> + * <li>Committer name/email: {@link CommitterRevFilter}</li> + * <li>Message body: {@link MessageRevFilter}</li> + * </ul> + * + * <p> + * <b>Merge filters:</b> + * <ul> + * <li>Skip all merges: {@link #NO_MERGES}.</li> + * </ul> + * + * <p> + * <b>Boolean modifiers:</b> + * <ul> + * <li>AND: {@link AndRevFilter}</li> + * <li>OR: {@link OrRevFilter}</li> + * <li>NOT: {@link NotRevFilter}</li> + * </ul> + */ +public abstract class RevFilter { + /** Default filter that always returns true (thread safe). */ + public static final RevFilter ALL = new RevFilter() { + @Override + public boolean include(final RevWalk walker, final RevCommit c) { + return true; + } + + @Override + public RevFilter clone() { + return this; + } + + @Override + public String toString() { + return "ALL"; + } + }; + + /** Default filter that always returns false (thread safe). */ + public static final RevFilter NONE = new RevFilter() { + @Override + public boolean include(final RevWalk walker, final RevCommit c) { + return false; + } + + @Override + public RevFilter clone() { + return this; + } + + @Override + public String toString() { + return "NONE"; + } + }; + + /** Excludes commits with more than one parent (thread safe). */ + public static final RevFilter NO_MERGES = new RevFilter() { + @Override + public boolean include(final RevWalk walker, final RevCommit c) { + return c.getParentCount() < 2; + } + + @Override + public RevFilter clone() { + return this; + } + + @Override + public String toString() { + return "NO_MERGES"; + } + }; + + /** + * Selects only merge bases of the starting points (thread safe). + * <p> + * This is a special case filter that cannot be combined with any other + * filter. Its include method always throws an exception as context + * information beyond the arguments is necessary to determine if the + * supplied commit is a merge base. + */ + public static final RevFilter MERGE_BASE = new RevFilter() { + @Override + public boolean include(final RevWalk walker, final RevCommit c) { + throw new UnsupportedOperationException("Cannot be combined."); + } + + @Override + public RevFilter clone() { + return this; + } + + @Override + public String toString() { + return "MERGE_BASE"; + } + }; + + /** + * Create a new filter that does the opposite of this filter. + * + * @return a new filter that includes commits this filter rejects. + */ + public RevFilter negate() { + return NotRevFilter.create(this); + } + + /** + * Determine if the supplied commit should be included in results. + * + * @param walker + * the active walker this filter is being invoked from within. + * @param cmit + * the commit currently being tested. The commit has been parsed + * and its body is available for inspection. + * @return true to include this commit in the results; false to have this + * commit be omitted entirely from the results. + * @throws StopWalkException + * the filter knows for certain that no additional commits can + * ever match, and the current commit doesn't match either. The + * walk is halted and no more results are provided. + * @throws MissingObjectException + * an object the filter needs to consult to determine its answer + * does not exist in the Git repository the walker is operating + * on. Filtering this commit is impossible without the object. + * @throws IncorrectObjectTypeException + * an object the filter needed to consult was not of the + * expected object type. This usually indicates a corrupt + * repository, as an object link is referencing the wrong type. + * @throws IOException + * a loose object or pack file could not be read to obtain data + * necessary for the filter to make its decision. + */ + public abstract boolean include(RevWalk walker, RevCommit cmit) + throws StopWalkException, MissingObjectException, + IncorrectObjectTypeException, IOException; + + /** + * Clone this revision filter, including its parameters. + * <p> + * This is a deep clone. If this filter embeds objects or other filters it + * must also clone those, to ensure the instances do not share mutable data. + * + * @return another copy of this filter, suitable for another thread. + */ + public abstract RevFilter clone(); + + @Override + public String toString() { + String n = getClass().getName(); + int lastDot = n.lastIndexOf('.'); + if (lastDot >= 0) { + n = n.substring(lastDot + 1); + } + return n.replace('$', '.'); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/RevFlagFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/RevFlagFilter.java new file mode 100644 index 0000000000..8339fc7c01 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/RevFlagFilter.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk.filter; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevFlagSet; +import org.eclipse.jgit.revwalk.RevWalk; + +/** Matches only commits with some/all RevFlags already set. */ +public abstract class RevFlagFilter extends RevFilter { + /** + * Create a new filter that tests for a single flag. + * + * @param a + * the flag to test. + * @return filter that selects only commits with flag <code>a</code>. + */ + public static RevFilter has(final RevFlag a) { + final RevFlagSet s = new RevFlagSet(); + s.add(a); + return new HasAll(s); + } + + /** + * Create a new filter that tests all flags in a set. + * + * @param a + * set of flags to test. + * @return filter that selects only commits with all flags in <code>a</code>. + */ + public static RevFilter hasAll(final RevFlag... a) { + final RevFlagSet set = new RevFlagSet(); + for (final RevFlag flag : a) + set.add(flag); + return new HasAll(set); + } + + /** + * Create a new filter that tests all flags in a set. + * + * @param a + * set of flags to test. + * @return filter that selects only commits with all flags in <code>a</code>. + */ + public static RevFilter hasAll(final RevFlagSet a) { + return new HasAll(new RevFlagSet(a)); + } + + /** + * Create a new filter that tests for any flag in a set. + * + * @param a + * set of flags to test. + * @return filter that selects only commits with any flag in <code>a</code>. + */ + public static RevFilter hasAny(final RevFlag... a) { + final RevFlagSet set = new RevFlagSet(); + for (final RevFlag flag : a) + set.add(flag); + return new HasAny(set); + } + + /** + * Create a new filter that tests for any flag in a set. + * + * @param a + * set of flags to test. + * @return filter that selects only commits with any flag in <code>a</code>. + */ + public static RevFilter hasAny(final RevFlagSet a) { + return new HasAny(new RevFlagSet(a)); + } + + final RevFlagSet flags; + + RevFlagFilter(final RevFlagSet m) { + flags = m; + } + + @Override + public RevFilter clone() { + return this; + } + + @Override + public String toString() { + return super.toString() + flags; + } + + private static class HasAll extends RevFlagFilter { + HasAll(final RevFlagSet m) { + super(m); + } + + @Override + public boolean include(final RevWalk walker, final RevCommit c) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + return c.hasAll(flags); + } + } + + private static class HasAny extends RevFlagFilter { + HasAny(final RevFlagSet m) { + super(m); + } + + @Override + public boolean include(final RevWalk walker, final RevCommit c) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + return c.hasAny(flags); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/SubStringRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/SubStringRevFilter.java new file mode 100644 index 0000000000..c6f421784a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/SubStringRevFilter.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.revwalk.filter; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.util.RawCharSequence; +import org.eclipse.jgit.util.RawSubStringPattern; + +/** Abstract filter that searches text using only substring search. */ +public abstract class SubStringRevFilter extends RevFilter { + /** + * Can this string be safely handled by a substring filter? + * + * @param pattern + * the pattern text proposed by the user. + * @return true if a substring filter can perform this pattern match; false + * if {@link PatternMatchRevFilter} must be used instead. + */ + public static boolean safe(final String pattern) { + for (int i = 0; i < pattern.length(); i++) { + final char c = pattern.charAt(i); + switch (c) { + case '.': + case '?': + case '*': + case '+': + case '{': + case '}': + case '(': + case ')': + case '[': + case ']': + case '\\': + return false; + } + } + return true; + } + + private final RawSubStringPattern pattern; + + /** + * Construct a new matching filter. + * + * @param patternText + * text to locate. This should be a safe string as described by + * the {@link #safe(String)} as regular expression meta + * characters are treated as literals. + */ + protected SubStringRevFilter(final String patternText) { + pattern = new RawSubStringPattern(patternText); + } + + @Override + public boolean include(final RevWalk walker, final RevCommit cmit) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + return pattern.match(text(cmit)) >= 0; + } + + /** + * Obtain the raw text to match against. + * + * @param cmit + * current commit being evaluated. + * @return sequence for the commit's content that we need to match on. + */ + protected abstract RawCharSequence text(RevCommit cmit); + + @Override + public RevFilter clone() { + return this; // Typically we are actually thread-safe. + } + + @Override + public String toString() { + return super.toString() + "(\"" + pattern.pattern() + "\")"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java new file mode 100644 index 0000000000..9343f4aba4 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java @@ -0,0 +1,795 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.URL; +import java.net.URLConnection; +import java.security.DigestOutputStream; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.SortedMap; +import java.util.TimeZone; +import java.util.TreeMap; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.eclipse.jgit.awtui.AwtAuthenticator; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.util.Base64; +import org.eclipse.jgit.util.HttpSupport; +import org.eclipse.jgit.util.StringUtils; +import org.eclipse.jgit.util.TemporaryBuffer; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; +import org.xml.sax.helpers.XMLReaderFactory; + +/** + * A simple HTTP REST client for the Amazon S3 service. + * <p> + * This client uses the REST API to communicate with the Amazon S3 servers and + * read or write content through a bucket that the user has access to. It is a + * very lightweight implementation of the S3 API and therefore does not have all + * of the bells and whistles of popular client implementations. + * <p> + * Authentication is always performed using the user's AWSAccessKeyId and their + * private AWSSecretAccessKey. + * <p> + * Optional client-side encryption may be enabled if requested. The format is + * compatible with <a href="http://jets3t.s3.amazonaws.com/index.html">jets3t</a>, + * a popular Java based Amazon S3 client library. Enabling encryption can hide + * sensitive data from the operators of the S3 service. + */ +public class AmazonS3 { + private static final Set<String> SIGNED_HEADERS; + + private static final String HMAC = "HmacSHA1"; + + private static final String DOMAIN = "s3.amazonaws.com"; + + private static final String X_AMZ_ACL = "x-amz-acl"; + + private static final String X_AMZ_META = "x-amz-meta-"; + + static { + SIGNED_HEADERS = new HashSet<String>(); + SIGNED_HEADERS.add("content-type"); + SIGNED_HEADERS.add("content-md5"); + SIGNED_HEADERS.add("date"); + } + + private static boolean isSignedHeader(final String name) { + final String nameLC = StringUtils.toLowerCase(name); + return SIGNED_HEADERS.contains(nameLC) || nameLC.startsWith("x-amz-"); + } + + private static String toCleanString(final List<String> list) { + final StringBuilder s = new StringBuilder(); + for (final String v : list) { + if (s.length() > 0) + s.append(','); + s.append(v.replaceAll("\n", "").trim()); + } + return s.toString(); + } + + private static String remove(final Map<String, String> m, final String k) { + final String r = m.remove(k); + return r != null ? r : ""; + } + + private static String httpNow() { + final String tz = "GMT"; + final SimpleDateFormat fmt; + fmt = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US); + fmt.setTimeZone(TimeZone.getTimeZone(tz)); + return fmt.format(new Date()) + " " + tz; + } + + private static MessageDigest newMD5() { + try { + return MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("JRE lacks MD5 implementation", e); + } + } + + /** AWSAccessKeyId, public string that identifies the user's account. */ + private final String publicKey; + + /** Decoded form of the private AWSSecretAccessKey, to sign requests. */ + private final SecretKeySpec privateKey; + + /** Our HTTP proxy support, in case we are behind a firewall. */ + private final ProxySelector proxySelector; + + /** ACL to apply to created objects. */ + private final String acl; + + /** Maximum number of times to try an operation. */ + private final int maxAttempts; + + /** Encryption algorithm, may be a null instance that provides pass-through. */ + private final WalkEncryption encryption; + + /** + * Create a new S3 client for the supplied user information. + * <p> + * The connection properties are a subset of those supported by the popular + * <a href="http://jets3t.s3.amazonaws.com/index.html">jets3t</a> library. + * For example: + * + * <pre> + * # AWS Access and Secret Keys (required) + * accesskey: <YourAWSAccessKey> + * secretkey: <YourAWSSecretKey> + * + * # Access Control List setting to apply to uploads, must be one of: + * # PRIVATE, PUBLIC_READ (defaults to PRIVATE). + * acl: PRIVATE + * + * # Number of times to retry after internal error from S3. + * httpclient.retry-max: 3 + * + * # End-to-end encryption (hides content from S3 owners) + * password: <encryption pass-phrase> + * crypto.algorithm: PBEWithMD5AndDES + * </pre> + * + * @param props + * connection properties. + * + */ + public AmazonS3(final Properties props) { + publicKey = props.getProperty("accesskey"); + if (publicKey == null) + throw new IllegalArgumentException("Missing accesskey."); + + final String secret = props.getProperty("secretkey"); + if (secret == null) + throw new IllegalArgumentException("Missing secretkey."); + privateKey = new SecretKeySpec(Constants.encodeASCII(secret), HMAC); + + final String pacl = props.getProperty("acl", "PRIVATE"); + if (StringUtils.equalsIgnoreCase("PRIVATE", pacl)) + acl = "private"; + else if (StringUtils.equalsIgnoreCase("PUBLIC", pacl)) + acl = "public-read"; + else if (StringUtils.equalsIgnoreCase("PUBLIC-READ", pacl)) + acl = "public-read"; + else if (StringUtils.equalsIgnoreCase("PUBLIC_READ", pacl)) + acl = "public-read"; + else + throw new IllegalArgumentException("Invalid acl: " + pacl); + + try { + final String cPas = props.getProperty("password"); + if (cPas != null) { + String cAlg = props.getProperty("crypto.algorithm"); + if (cAlg == null) + cAlg = "PBEWithMD5AndDES"; + encryption = new WalkEncryption.ObjectEncryptionV2(cAlg, cPas); + } else { + encryption = WalkEncryption.NONE; + } + } catch (InvalidKeySpecException e) { + throw new IllegalArgumentException("Invalid encryption", e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Invalid encryption", e); + } + + maxAttempts = Integer.parseInt(props.getProperty( + "httpclient.retry-max", "3")); + proxySelector = ProxySelector.getDefault(); + } + + /** + * Get the content of a bucket object. + * + * @param bucket + * name of the bucket storing the object. + * @param key + * key of the object within its bucket. + * @return connection to stream the content of the object. The request + * properties of the connection may not be modified by the caller as + * the request parameters have already been signed. + * @throws IOException + * sending the request was not possible. + */ + public URLConnection get(final String bucket, final String key) + throws IOException { + for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { + final HttpURLConnection c = open("GET", bucket, key); + authorize(c); + switch (HttpSupport.response(c)) { + case HttpURLConnection.HTTP_OK: + encryption.validate(c, X_AMZ_META); + return c; + case HttpURLConnection.HTTP_NOT_FOUND: + throw new FileNotFoundException(key); + case HttpURLConnection.HTTP_INTERNAL_ERROR: + continue; + default: + throw error("Reading", key, c); + } + } + throw maxAttempts("Reading", key); + } + + /** + * Decrypt an input stream from {@link #get(String, String)}. + * + * @param u + * connection previously created by {@link #get(String, String)}}. + * @return stream to read plain text from. + * @throws IOException + * decryption could not be configured. + */ + public InputStream decrypt(final URLConnection u) throws IOException { + return encryption.decrypt(u.getInputStream()); + } + + /** + * List the names of keys available within a bucket. + * <p> + * This method is primarily meant for obtaining a "recursive directory + * listing" rooted under the specified bucket and prefix location. + * + * @param bucket + * name of the bucket whose objects should be listed. + * @param prefix + * common prefix to filter the results by. Must not be null. + * Supplying the empty string will list all keys in the bucket. + * Supplying a non-empty string will act as though a trailing '/' + * appears in prefix, even if it does not. + * @return list of keys starting with <code>prefix</code>, after removing + * <code>prefix</code> (or <code>prefix + "/"</code>)from all + * of them. + * @throws IOException + * sending the request was not possible, or the response XML + * document could not be parsed properly. + */ + public List<String> list(final String bucket, String prefix) + throws IOException { + if (prefix.length() > 0 && !prefix.endsWith("/")) + prefix += "/"; + final ListParser lp = new ListParser(bucket, prefix); + do { + lp.list(); + } while (lp.truncated); + return lp.entries; + } + + /** + * Delete a single object. + * <p> + * Deletion always succeeds, even if the object does not exist. + * + * @param bucket + * name of the bucket storing the object. + * @param key + * key of the object within its bucket. + * @throws IOException + * deletion failed due to communications error. + */ + public void delete(final String bucket, final String key) + throws IOException { + for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { + final HttpURLConnection c = open("DELETE", bucket, key); + authorize(c); + switch (HttpSupport.response(c)) { + case HttpURLConnection.HTTP_NO_CONTENT: + return; + case HttpURLConnection.HTTP_INTERNAL_ERROR: + continue; + default: + throw error("Deletion", key, c); + } + } + throw maxAttempts("Deletion", key); + } + + /** + * Atomically create or replace a single small object. + * <p> + * This form is only suitable for smaller contents, where the caller can + * reasonable fit the entire thing into memory. + * <p> + * End-to-end data integrity is assured by internally computing the MD5 + * checksum of the supplied data and transmitting the checksum along with + * the data itself. + * + * @param bucket + * name of the bucket storing the object. + * @param key + * key of the object within its bucket. + * @param data + * new data content for the object. Must not be null. Zero length + * array will create a zero length object. + * @throws IOException + * creation/updating failed due to communications error. + */ + public void put(final String bucket, final String key, final byte[] data) + throws IOException { + if (encryption != WalkEncryption.NONE) { + // We have to copy to produce the cipher text anyway so use + // the large object code path as it supports that behavior. + // + final OutputStream os = beginPut(bucket, key, null, null); + os.write(data); + os.close(); + return; + } + + final String md5str = Base64.encodeBytes(newMD5().digest(data)); + final String lenstr = String.valueOf(data.length); + for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { + final HttpURLConnection c = open("PUT", bucket, key); + c.setRequestProperty("Content-Length", lenstr); + c.setRequestProperty("Content-MD5", md5str); + c.setRequestProperty(X_AMZ_ACL, acl); + authorize(c); + c.setDoOutput(true); + c.setFixedLengthStreamingMode(data.length); + final OutputStream os = c.getOutputStream(); + try { + os.write(data); + } finally { + os.close(); + } + + switch (HttpSupport.response(c)) { + case HttpURLConnection.HTTP_OK: + return; + case HttpURLConnection.HTTP_INTERNAL_ERROR: + continue; + default: + throw error("Writing", key, c); + } + } + throw maxAttempts("Writing", key); + } + + /** + * Atomically create or replace a single large object. + * <p> + * Initially the returned output stream buffers data into memory, but if the + * total number of written bytes starts to exceed an internal limit the data + * is spooled to a temporary file on the local drive. + * <p> + * Network transmission is attempted only when <code>close()</code> gets + * called at the end of output. Closing the returned stream can therefore + * take significant time, especially if the written content is very large. + * <p> + * End-to-end data integrity is assured by internally computing the MD5 + * checksum of the supplied data and transmitting the checksum along with + * the data itself. + * + * @param bucket + * name of the bucket storing the object. + * @param key + * key of the object within its bucket. + * @param monitor + * (optional) progress monitor to post upload completion to + * during the stream's close method. + * @param monitorTask + * (optional) task name to display during the close method. + * @return a stream which accepts the new data, and transmits once closed. + * @throws IOException + * if encryption was enabled it could not be configured. + */ + public OutputStream beginPut(final String bucket, final String key, + final ProgressMonitor monitor, final String monitorTask) + throws IOException { + final MessageDigest md5 = newMD5(); + final TemporaryBuffer buffer = new TemporaryBuffer() { + @Override + public void close() throws IOException { + super.close(); + try { + putImpl(bucket, key, md5.digest(), this, monitor, + monitorTask); + } finally { + destroy(); + } + } + }; + return encryption.encrypt(new DigestOutputStream(buffer, md5)); + } + + private void putImpl(final String bucket, final String key, + final byte[] csum, final TemporaryBuffer buf, + ProgressMonitor monitor, String monitorTask) throws IOException { + if (monitor == null) + monitor = NullProgressMonitor.INSTANCE; + if (monitorTask == null) + monitorTask = "Uploading " + key; + + final String md5str = Base64.encodeBytes(csum); + final long len = buf.length(); + final String lenstr = String.valueOf(len); + for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { + final HttpURLConnection c = open("PUT", bucket, key); + c.setRequestProperty("Content-Length", lenstr); + c.setRequestProperty("Content-MD5", md5str); + c.setRequestProperty(X_AMZ_ACL, acl); + encryption.request(c, X_AMZ_META); + authorize(c); + c.setDoOutput(true); + c.setFixedLengthStreamingMode((int) len); + monitor.beginTask(monitorTask, (int) (len / 1024)); + final OutputStream os = c.getOutputStream(); + try { + buf.writeTo(os, monitor); + } finally { + monitor.endTask(); + os.close(); + } + + switch (HttpSupport.response(c)) { + case HttpURLConnection.HTTP_OK: + return; + case HttpURLConnection.HTTP_INTERNAL_ERROR: + continue; + default: + throw error("Writing", key, c); + } + } + throw maxAttempts("Writing", key); + } + + private IOException error(final String action, final String key, + final HttpURLConnection c) throws IOException { + final IOException err = new IOException(action + " of '" + key + + "' failed: " + HttpSupport.response(c) + " " + + c.getResponseMessage()); + final ByteArrayOutputStream b = new ByteArrayOutputStream(); + byte[] buf = new byte[2048]; + for (;;) { + final int n = c.getErrorStream().read(buf); + if (n < 0) + break; + if (n > 0) + b.write(buf, 0, n); + } + buf = b.toByteArray(); + if (buf.length > 0) + err.initCause(new IOException("\n" + new String(buf))); + return err; + } + + private IOException maxAttempts(final String action, final String key) { + return new IOException(action + " of '" + key + "' failed:" + + " Giving up after " + maxAttempts + " attempts."); + } + + private HttpURLConnection open(final String method, final String bucket, + final String key) throws IOException { + final Map<String, String> noArgs = Collections.emptyMap(); + return open(method, bucket, key, noArgs); + } + + private HttpURLConnection open(final String method, final String bucket, + final String key, final Map<String, String> args) + throws IOException { + final StringBuilder urlstr = new StringBuilder(); + urlstr.append("http://"); + urlstr.append(bucket); + urlstr.append('.'); + urlstr.append(DOMAIN); + urlstr.append('/'); + if (key.length() > 0) + HttpSupport.encode(urlstr, key); + if (!args.isEmpty()) { + final Iterator<Map.Entry<String, String>> i; + + urlstr.append('?'); + i = args.entrySet().iterator(); + while (i.hasNext()) { + final Map.Entry<String, String> e = i.next(); + urlstr.append(e.getKey()); + urlstr.append('='); + HttpSupport.encode(urlstr, e.getValue()); + if (i.hasNext()) + urlstr.append('&'); + } + } + + final URL url = new URL(urlstr.toString()); + final Proxy proxy = HttpSupport.proxyFor(proxySelector, url); + final HttpURLConnection c; + + c = (HttpURLConnection) url.openConnection(proxy); + c.setRequestMethod(method); + c.setRequestProperty("User-Agent", "jgit/1.0"); + c.setRequestProperty("Date", httpNow()); + return c; + } + + private void authorize(final HttpURLConnection c) throws IOException { + final Map<String, List<String>> reqHdr = c.getRequestProperties(); + final SortedMap<String, String> sigHdr = new TreeMap<String, String>(); + for (final Map.Entry<String, List<String>> entry : reqHdr.entrySet()) { + final String hdr = entry.getKey(); + if (isSignedHeader(hdr)) + sigHdr.put(StringUtils.toLowerCase(hdr), toCleanString(entry.getValue())); + } + + final StringBuilder s = new StringBuilder(); + s.append(c.getRequestMethod()); + s.append('\n'); + + s.append(remove(sigHdr, "content-md5")); + s.append('\n'); + + s.append(remove(sigHdr, "content-type")); + s.append('\n'); + + s.append(remove(sigHdr, "date")); + s.append('\n'); + + for (final Map.Entry<String, String> e : sigHdr.entrySet()) { + s.append(e.getKey()); + s.append(':'); + s.append(e.getValue()); + s.append('\n'); + } + + final String host = c.getURL().getHost(); + s.append('/'); + s.append(host.substring(0, host.length() - DOMAIN.length() - 1)); + s.append(c.getURL().getPath()); + + final String sec; + try { + final Mac m = Mac.getInstance(HMAC); + m.init(privateKey); + sec = Base64.encodeBytes(m.doFinal(s.toString().getBytes("UTF-8"))); + } catch (NoSuchAlgorithmException e) { + throw new IOException("No " + HMAC + " support:" + e.getMessage()); + } catch (InvalidKeyException e) { + throw new IOException("Invalid key: " + e.getMessage()); + } + c.setRequestProperty("Authorization", "AWS " + publicKey + ":" + sec); + } + + /** + * Simple command line interface to {@link AmazonS3}. + * + * @param argv + * command line arguments. See usage for details. + * @throws IOException + * an error occurred. + */ + public static void main(final String[] argv) throws IOException { + if (argv.length != 4) { + commandLineUsage(); + return; + } + + AwtAuthenticator.install(); + HttpSupport.configureHttpProxy(); + + final AmazonS3 s3 = new AmazonS3(properties(new File(argv[0]))); + final String op = argv[1]; + final String bucket = argv[2]; + final String key = argv[3]; + if ("get".equals(op)) { + final URLConnection c = s3.get(bucket, key); + int len = c.getContentLength(); + final InputStream in = c.getInputStream(); + try { + final byte[] tmp = new byte[2048]; + while (len > 0) { + final int n = in.read(tmp); + if (n < 0) + throw new EOFException("Expected " + len + " bytes."); + System.out.write(tmp, 0, n); + len -= n; + } + } finally { + in.close(); + } + } else if ("ls".equals(op) || "list".equals(op)) { + for (final String k : s3.list(bucket, key)) + System.out.println(k); + } else if ("rm".equals(op) || "delete".equals(op)) { + s3.delete(bucket, key); + } else if ("put".equals(op)) { + final OutputStream os = s3.beginPut(bucket, key, null, null); + final byte[] tmp = new byte[2048]; + int n; + while ((n = System.in.read(tmp)) > 0) + os.write(tmp, 0, n); + os.close(); + } else { + commandLineUsage(); + } + } + + private static void commandLineUsage() { + System.err.println("usage: conn.prop op bucket key"); + System.err.println(); + System.err.println(" where conn.prop is a jets3t properties file."); + System.err.println(" op is one of: get ls rm put"); + System.err.println(" bucket is the name of the S3 bucket"); + System.err.println(" key is the name of the object."); + System.exit(1); + } + + static Properties properties(final File authFile) + throws FileNotFoundException, IOException { + final Properties p = new Properties(); + final FileInputStream in = new FileInputStream(authFile); + try { + p.load(in); + } finally { + in.close(); + } + return p; + } + + private final class ListParser extends DefaultHandler { + final List<String> entries = new ArrayList<String>(); + + private final String bucket; + + private final String prefix; + + boolean truncated; + + private StringBuilder data; + + ListParser(final String bn, final String p) { + bucket = bn; + prefix = p; + } + + void list() throws IOException { + final Map<String, String> args = new TreeMap<String, String>(); + if (prefix.length() > 0) + args.put("prefix", prefix); + if (!entries.isEmpty()) + args.put("marker", prefix + entries.get(entries.size() - 1)); + + for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { + final HttpURLConnection c = open("GET", bucket, "", args); + authorize(c); + switch (HttpSupport.response(c)) { + case HttpURLConnection.HTTP_OK: + truncated = false; + data = null; + + final XMLReader xr; + try { + xr = XMLReaderFactory.createXMLReader(); + } catch (SAXException e) { + throw new IOException("No XML parser available."); + } + xr.setContentHandler(this); + final InputStream in = c.getInputStream(); + try { + xr.parse(new InputSource(in)); + } catch (SAXException parsingError) { + final IOException p; + p = new IOException("Error listing " + prefix); + p.initCause(parsingError); + throw p; + } finally { + in.close(); + } + return; + + case HttpURLConnection.HTTP_INTERNAL_ERROR: + continue; + + default: + throw AmazonS3.this.error("Listing", prefix, c); + } + } + throw maxAttempts("Listing", prefix); + } + + @Override + public void startElement(final String uri, final String name, + final String qName, final Attributes attributes) + throws SAXException { + if ("Key".equals(name) || "IsTruncated".equals(name)) + data = new StringBuilder(); + } + + @Override + public void ignorableWhitespace(final char[] ch, final int s, + final int n) throws SAXException { + if (data != null) + data.append(ch, s, n); + } + + @Override + public void characters(final char[] ch, final int s, final int n) + throws SAXException { + if (data != null) + data.append(ch, s, n); + } + + @Override + public void endElement(final String uri, final String name, + final String qName) throws SAXException { + if ("Key".equals(name)) + entries.add(data.toString().substring(prefix.length())); + else if ("IsTruncated".equals(name)) + truncated = StringUtils.equalsIgnoreCase("true", data.toString()); + data = null; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java new file mode 100644 index 0000000000..14be1700c8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Ref; + +/** + * Base helper class for implementing operations connections. + * + * @see BasePackConnection + * @see BaseFetchConnection + */ +abstract class BaseConnection implements Connection { + + private Map<String, Ref> advertisedRefs = Collections.emptyMap(); + + private boolean startedOperation; + + public Map<String, Ref> getRefsMap() { + return advertisedRefs; + } + + public final Collection<Ref> getRefs() { + return advertisedRefs.values(); + } + + public final Ref getRef(final String name) { + return advertisedRefs.get(name); + } + + public abstract void close(); + + /** + * Denote the list of refs available on the remote repository. + * <p> + * Implementors should invoke this method once they have obtained the refs + * that are available from the remote repository. + * + * @param all + * the complete list of refs the remote has to offer. This map + * will be wrapped in an unmodifiable way to protect it, but it + * does not get copied. + */ + protected void available(final Map<String, Ref> all) { + advertisedRefs = Collections.unmodifiableMap(all); + } + + /** + * Helper method for ensuring one-operation per connection. Check whether + * operation was already marked as started, and mark it as started. + * + * @throws TransportException + * if operation was already marked as started. + */ + protected void markStartedOperation() throws TransportException { + if (startedOperation) + throw new TransportException( + "Only one operation call per connection is supported."); + startedOperation = true; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseFetchConnection.java new file mode 100644 index 0000000000..b77e644a25 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseFetchConnection.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.util.Collection; +import java.util.Set; + +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; + +/** + * Base helper class for fetch connection implementations. Provides some common + * typical structures and methods used during fetch connection. + * <p> + * Implementors of fetch over pack-based protocols should consider using + * {@link BasePackFetchConnection} instead. + * </p> + */ +abstract class BaseFetchConnection extends BaseConnection implements + FetchConnection { + public final void fetch(final ProgressMonitor monitor, + final Collection<Ref> want, final Set<ObjectId> have) + throws TransportException { + markStartedOperation(); + doFetch(monitor, want, have); + } + + /** + * Default implementation of {@link FetchConnection#didFetchIncludeTags()} - + * returning false. + */ + public boolean didFetchIncludeTags() { + return false; + } + + /** + * Implementation of {@link #fetch(ProgressMonitor, Collection, Set)} + * without checking for multiple fetch. + * + * @param monitor + * as in {@link #fetch(ProgressMonitor, Collection, Set)} + * @param want + * as in {@link #fetch(ProgressMonitor, Collection, Set)} + * @param have + * as in {@link #fetch(ProgressMonitor, Collection, Set)} + * @throws TransportException + * as in {@link #fetch(ProgressMonitor, Collection, Set)}, but + * implementation doesn't have to care about multiple + * {@link #fetch(ProgressMonitor, Collection, Set)} calls, as it + * is checked in this class. + */ + protected abstract void doFetch(final ProgressMonitor monitor, + final Collection<Ref> want, final Set<ObjectId> have) + throws TransportException; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java new file mode 100644 index 0000000000..bd86ec0472 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Set; + +import org.eclipse.jgit.errors.NoRemoteRepositoryException; +import org.eclipse.jgit.errors.PackProtocolException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.io.InterruptTimer; +import org.eclipse.jgit.util.io.TimeoutInputStream; +import org.eclipse.jgit.util.io.TimeoutOutputStream; + +/** + * Base helper class for pack-based operations implementations. Provides partial + * implementation of pack-protocol - refs advertising and capabilities support, + * and some other helper methods. + * + * @see BasePackFetchConnection + * @see BasePackPushConnection + */ +abstract class BasePackConnection extends BaseConnection { + + /** The repository this transport fetches into, or pushes out of. */ + protected final Repository local; + + /** Remote repository location. */ + protected final URIish uri; + + /** A transport connected to {@link #uri}. */ + protected final Transport transport; + + /** Low-level input stream, if a timeout was configured. */ + protected TimeoutInputStream timeoutIn; + + /** Low-level output stream, if a timeout was configured. */ + protected TimeoutOutputStream timeoutOut; + + /** Timer to manage {@link #timeoutIn} and {@link #timeoutOut}. */ + private InterruptTimer myTimer; + + /** Buffered input stream reading from the remote. */ + protected InputStream in; + + /** Buffered output stream sending to the remote. */ + protected OutputStream out; + + /** Packet line decoder around {@link #in}. */ + protected PacketLineIn pckIn; + + /** Packet line encoder around {@link #out}. */ + protected PacketLineOut pckOut; + + /** Send {@link PacketLineOut#end()} before closing {@link #out}? */ + protected boolean outNeedsEnd; + + /** Capability tokens advertised by the remote side. */ + private final Set<String> remoteCapablities = new HashSet<String>(); + + /** Extra objects the remote has, but which aren't offered as refs. */ + protected final Set<ObjectId> additionalHaves = new HashSet<ObjectId>(); + + BasePackConnection(final PackTransport packTransport) { + transport = (Transport)packTransport; + local = transport.local; + uri = transport.uri; + } + + protected final void init(InputStream myIn, OutputStream myOut) { + final int timeout = transport.getTimeout(); + if (timeout > 0) { + final Thread caller = Thread.currentThread(); + myTimer = new InterruptTimer(caller.getName() + "-Timer"); + timeoutIn = new TimeoutInputStream(myIn, myTimer); + timeoutOut = new TimeoutOutputStream(myOut, myTimer); + timeoutIn.setTimeout(timeout * 1000); + timeoutOut.setTimeout(timeout * 1000); + myIn = timeoutIn; + myOut = timeoutOut; + } + + in = myIn instanceof BufferedInputStream ? myIn + : new BufferedInputStream(myIn, IndexPack.BUFFER_SIZE); + out = myOut instanceof BufferedOutputStream ? myOut + : new BufferedOutputStream(myOut); + + pckIn = new PacketLineIn(in); + pckOut = new PacketLineOut(out); + outNeedsEnd = true; + } + + protected void readAdvertisedRefs() throws TransportException { + try { + readAdvertisedRefsImpl(); + } catch (TransportException err) { + close(); + throw err; + } catch (IOException err) { + close(); + throw new TransportException(err.getMessage(), err); + } catch (RuntimeException err) { + close(); + throw new TransportException(err.getMessage(), err); + } + } + + private void readAdvertisedRefsImpl() throws IOException { + final LinkedHashMap<String, Ref> avail = new LinkedHashMap<String, Ref>(); + for (;;) { + String line; + + try { + line = pckIn.readString(); + } catch (EOFException eof) { + if (avail.isEmpty()) + throw noRepository(); + throw eof; + } + if (line == PacketLineIn.END) + break; + + if (avail.isEmpty()) { + final int nul = line.indexOf('\0'); + if (nul >= 0) { + // The first line (if any) may contain "hidden" + // capability values after a NUL byte. + for (String c : line.substring(nul + 1).split(" ")) + remoteCapablities.add(c); + line = line.substring(0, nul); + } + } + + String name = line.substring(41, line.length()); + if (avail.isEmpty() && name.equals("capabilities^{}")) { + // special line from git-receive-pack to show + // capabilities when there are no refs to advertise + continue; + } + + final ObjectId id = ObjectId.fromString(line.substring(0, 40)); + if (name.equals(".have")) { + additionalHaves.add(id); + } else if (name.endsWith("^{}")) { + name = name.substring(0, name.length() - 3); + final Ref prior = avail.get(name); + if (prior == null) + throw new PackProtocolException(uri, "advertisement of " + + name + "^{} came before " + name); + + if (prior.getPeeledObjectId() != null) + throw duplicateAdvertisement(name + "^{}"); + + avail.put(name, new Ref(Ref.Storage.NETWORK, name, prior + .getObjectId(), id, true)); + } else { + final Ref prior; + prior = avail.put(name, new Ref(Ref.Storage.NETWORK, name, id)); + if (prior != null) + throw duplicateAdvertisement(name); + } + } + available(avail); + } + + /** + * Create an exception to indicate problems finding a remote repository. The + * caller is expected to throw the returned exception. + * + * Subclasses may override this method to provide better diagnostics. + * + * @return a TransportException saying a repository cannot be found and + * possibly why. + */ + protected TransportException noRepository() { + return new NoRemoteRepositoryException(uri, "not found."); + } + + protected boolean isCapableOf(final String option) { + return remoteCapablities.contains(option); + } + + protected boolean wantCapability(final StringBuilder b, final String option) { + if (!isCapableOf(option)) + return false; + b.append(' '); + b.append(option); + return true; + } + + private PackProtocolException duplicateAdvertisement(final String name) { + return new PackProtocolException(uri, "duplicate advertisements of " + + name); + } + + @Override + public void close() { + if (out != null) { + try { + if (outNeedsEnd) + pckOut.end(); + out.close(); + } catch (IOException err) { + // Ignore any close errors. + } finally { + out = null; + pckOut = null; + } + } + + if (in != null) { + try { + in.close(); + } catch (IOException err) { + // Ignore any close errors. + } finally { + in = null; + pckIn = null; + } + } + + if (myTimer != null) { + try { + myTimer.terminate(); + } finally { + myTimer = null; + timeoutIn = null; + timeoutOut = null; + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java new file mode 100644 index 0000000000..3f478680aa --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Set; + +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.MutableObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PackLock; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Config.SectionParser; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevCommitList; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevSort; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter; +import org.eclipse.jgit.revwalk.filter.RevFilter; + +/** + * Fetch implementation using the native Git pack transfer service. + * <p> + * This is the canonical implementation for transferring objects from the remote + * repository to the local repository by talking to the 'git-upload-pack' + * service. Objects are packed on the remote side into a pack file and then sent + * down the pipe to us. + * <p> + * This connection requires only a bi-directional pipe or socket, and thus is + * easily wrapped up into a local process pipe, anonymous TCP socket, or a + * command executed through an SSH tunnel. + * <p> + * Concrete implementations should just call + * {@link #init(java.io.InputStream, java.io.OutputStream)} and + * {@link #readAdvertisedRefs()} methods in constructor or before any use. They + * should also handle resources releasing in {@link #close()} method if needed. + */ +abstract class BasePackFetchConnection extends BasePackConnection implements + FetchConnection { + /** + * Maximum number of 'have' lines to send before giving up. + * <p> + * During {@link #negotiate(ProgressMonitor)} we send at most this many + * commits to the remote peer as 'have' lines without an ACK response before + * we give up. + */ + private static final int MAX_HAVES = 256; + + /** + * Amount of data the client sends before starting to read. + * <p> + * Any output stream given to the client must be able to buffer this many + * bytes before the client will stop writing and start reading from the + * input stream. If the output stream blocks before this many bytes are in + * the send queue, the system will deadlock. + */ + protected static final int MIN_CLIENT_BUFFER = 2 * 32 * 46 + 8; + + static final String OPTION_INCLUDE_TAG = "include-tag"; + + static final String OPTION_MULTI_ACK = "multi_ack"; + + static final String OPTION_THIN_PACK = "thin-pack"; + + static final String OPTION_SIDE_BAND = "side-band"; + + static final String OPTION_SIDE_BAND_64K = "side-band-64k"; + + static final String OPTION_OFS_DELTA = "ofs-delta"; + + static final String OPTION_SHALLOW = "shallow"; + + static final String OPTION_NO_PROGRESS = "no-progress"; + + private final RevWalk walk; + + /** All commits that are immediately reachable by a local ref. */ + private RevCommitList<RevCommit> reachableCommits; + + /** Marks an object as having all its dependencies. */ + final RevFlag REACHABLE; + + /** Marks a commit known to both sides of the connection. */ + final RevFlag COMMON; + + /** Marks a commit listed in the advertised refs. */ + final RevFlag ADVERTISED; + + private boolean multiAck; + + private boolean thinPack; + + private boolean sideband; + + private boolean includeTags; + + private boolean allowOfsDelta; + + private String lockMessage; + + private PackLock packLock; + + BasePackFetchConnection(final PackTransport packTransport) { + super(packTransport); + + final FetchConfig cfg = local.getConfig().get(FetchConfig.KEY); + includeTags = transport.getTagOpt() != TagOpt.NO_TAGS; + thinPack = transport.isFetchThin(); + allowOfsDelta = cfg.allowOfsDelta; + + walk = new RevWalk(local); + reachableCommits = new RevCommitList<RevCommit>(); + REACHABLE = walk.newFlag("REACHABLE"); + COMMON = walk.newFlag("COMMON"); + ADVERTISED = walk.newFlag("ADVERTISED"); + + walk.carry(COMMON); + walk.carry(REACHABLE); + walk.carry(ADVERTISED); + } + + private static class FetchConfig { + static final SectionParser<FetchConfig> KEY = new SectionParser<FetchConfig>() { + public FetchConfig parse(final Config cfg) { + return new FetchConfig(cfg); + } + }; + + final boolean allowOfsDelta; + + FetchConfig(final Config c) { + allowOfsDelta = c.getBoolean("repack", "usedeltabaseoffset", true); + } + } + + public final void fetch(final ProgressMonitor monitor, + final Collection<Ref> want, final Set<ObjectId> have) + throws TransportException { + markStartedOperation(); + doFetch(monitor, want, have); + } + + public boolean didFetchIncludeTags() { + return false; + } + + public boolean didFetchTestConnectivity() { + return false; + } + + public void setPackLockMessage(final String message) { + lockMessage = message; + } + + public Collection<PackLock> getPackLocks() { + if (packLock != null) + return Collections.singleton(packLock); + return Collections.<PackLock> emptyList(); + } + + protected void doFetch(final ProgressMonitor monitor, + final Collection<Ref> want, final Set<ObjectId> have) + throws TransportException { + try { + markRefsAdvertised(); + markReachable(have, maxTimeWanted(want)); + + if (sendWants(want)) { + negotiate(monitor); + + walk.dispose(); + reachableCommits = null; + + receivePack(monitor); + } + } catch (CancelledException ce) { + close(); + return; // Caller should test (or just know) this themselves. + } catch (IOException err) { + close(); + throw new TransportException(err.getMessage(), err); + } catch (RuntimeException err) { + close(); + throw new TransportException(err.getMessage(), err); + } + } + + private int maxTimeWanted(final Collection<Ref> wants) { + int maxTime = 0; + for (final Ref r : wants) { + try { + final RevObject obj = walk.parseAny(r.getObjectId()); + if (obj instanceof RevCommit) { + final int cTime = ((RevCommit) obj).getCommitTime(); + if (maxTime < cTime) + maxTime = cTime; + } + } catch (IOException error) { + // We don't have it, but we want to fetch (thus fixing error). + } + } + return maxTime; + } + + private void markReachable(final Set<ObjectId> have, final int maxTime) + throws IOException { + for (final Ref r : local.getAllRefs().values()) { + try { + final RevCommit o = walk.parseCommit(r.getObjectId()); + o.add(REACHABLE); + reachableCommits.add(o); + } catch (IOException readError) { + // If we cannot read the value of the ref skip it. + } + } + + for (final ObjectId id : have) { + try { + final RevCommit o = walk.parseCommit(id); + o.add(REACHABLE); + reachableCommits.add(o); + } catch (IOException readError) { + // If we cannot read the value of the ref skip it. + } + } + + if (maxTime > 0) { + // Mark reachable commits until we reach maxTime. These may + // wind up later matching up against things we want and we + // can avoid asking for something we already happen to have. + // + final Date maxWhen = new Date(maxTime * 1000L); + walk.sort(RevSort.COMMIT_TIME_DESC); + walk.markStart(reachableCommits); + walk.setRevFilter(CommitTimeRevFilter.after(maxWhen)); + for (;;) { + final RevCommit c = walk.next(); + if (c == null) + break; + if (c.has(ADVERTISED) && !c.has(COMMON)) { + // This is actually going to be a common commit, but + // our peer doesn't know that fact yet. + // + c.add(COMMON); + c.carry(COMMON); + reachableCommits.add(c); + } + } + } + } + + private boolean sendWants(final Collection<Ref> want) throws IOException { + boolean first = true; + for (final Ref r : want) { + try { + if (walk.parseAny(r.getObjectId()).has(REACHABLE)) { + // We already have this object. Asking for it is + // not a very good idea. + // + continue; + } + } catch (IOException err) { + // Its OK, we don't have it, but we want to fix that + // by fetching the object from the other side. + } + + final StringBuilder line = new StringBuilder(46); + line.append("want "); + line.append(r.getObjectId().name()); + if (first) { + line.append(enableCapabilities()); + first = false; + } + line.append('\n'); + pckOut.writeString(line.toString()); + } + pckOut.end(); + outNeedsEnd = false; + return !first; + } + + private String enableCapabilities() { + final StringBuilder line = new StringBuilder(); + if (includeTags) + includeTags = wantCapability(line, OPTION_INCLUDE_TAG); + if (allowOfsDelta) + wantCapability(line, OPTION_OFS_DELTA); + multiAck = wantCapability(line, OPTION_MULTI_ACK); + if (thinPack) + thinPack = wantCapability(line, OPTION_THIN_PACK); + if (wantCapability(line, OPTION_SIDE_BAND_64K)) + sideband = true; + else if (wantCapability(line, OPTION_SIDE_BAND)) + sideband = true; + return line.toString(); + } + + private void negotiate(final ProgressMonitor monitor) throws IOException, + CancelledException { + final MutableObjectId ackId = new MutableObjectId(); + int resultsPending = 0; + int havesSent = 0; + int havesSinceLastContinue = 0; + boolean receivedContinue = false; + boolean receivedAck = false; + boolean sendHaves = true; + + negotiateBegin(); + while (sendHaves) { + final RevCommit c = walk.next(); + if (c == null) + break; + + pckOut.writeString("have " + c.getId().name() + "\n"); + havesSent++; + havesSinceLastContinue++; + + if ((31 & havesSent) != 0) { + // We group the have lines into blocks of 32, each marked + // with a flush (aka end). This one is within a block so + // continue with another have line. + // + continue; + } + + if (monitor.isCancelled()) + throw new CancelledException(); + + pckOut.end(); + resultsPending++; // Each end will cause a result to come back. + + if (havesSent == 32) { + // On the first block we race ahead and try to send + // more of the second block while waiting for the + // remote to respond to our first block request. + // This keeps us one block ahead of the peer. + // + continue; + } + + for (;;) { + final PacketLineIn.AckNackResult anr; + + anr = pckIn.readACK(ackId); + if (anr == PacketLineIn.AckNackResult.NAK) { + // More have lines are necessary to compute the + // pack on the remote side. Keep doing that. + // + resultsPending--; + break; + } + + if (anr == PacketLineIn.AckNackResult.ACK) { + // The remote side is happy and knows exactly what + // to send us. There is no further negotiation and + // we can break out immediately. + // + multiAck = false; + resultsPending = 0; + receivedAck = true; + sendHaves = false; + break; + } + + if (anr == PacketLineIn.AckNackResult.ACK_CONTINUE) { + // The server knows this commit (ackId). We don't + // need to send any further along its ancestry, but + // we need to continue to talk about other parts of + // our local history. + // + markCommon(walk.parseAny(ackId)); + receivedAck = true; + receivedContinue = true; + havesSinceLastContinue = 0; + } + + if (monitor.isCancelled()) + throw new CancelledException(); + } + + if (receivedContinue && havesSinceLastContinue > MAX_HAVES) { + // Our history must be really different from the remote's. + // We just sent a whole slew of have lines, and it did not + // recognize any of them. Avoid sending our entire history + // to them by giving up early. + // + break; + } + } + + // Tell the remote side we have run out of things to talk about. + // + if (monitor.isCancelled()) + throw new CancelledException(); + pckOut.writeString("done\n"); + pckOut.flush(); + + if (!receivedAck) { + // Apparently if we have never received an ACK earlier + // there is one more result expected from the done we + // just sent to the remote. + // + multiAck = false; + resultsPending++; + } + + while (resultsPending > 0 || multiAck) { + final PacketLineIn.AckNackResult anr; + + anr = pckIn.readACK(ackId); + resultsPending--; + + if (anr == PacketLineIn.AckNackResult.ACK) + break; // commit negotiation is finished. + + if (anr == PacketLineIn.AckNackResult.ACK_CONTINUE) { + // There must be a normal ACK following this. + // + multiAck = true; + } + + if (monitor.isCancelled()) + throw new CancelledException(); + } + } + + private void negotiateBegin() throws IOException { + walk.resetRetain(REACHABLE, ADVERTISED); + walk.markStart(reachableCommits); + walk.sort(RevSort.COMMIT_TIME_DESC); + walk.setRevFilter(new RevFilter() { + @Override + public RevFilter clone() { + return this; + } + + @Override + public boolean include(final RevWalk walker, final RevCommit c) { + final boolean remoteKnowsIsCommon = c.has(COMMON); + if (c.has(ADVERTISED)) { + // Remote advertised this, and we have it, hence common. + // Whether or not the remote knows that fact is tested + // before we added the flag. If the remote doesn't know + // we have to still send them this object. + // + c.add(COMMON); + } + return !remoteKnowsIsCommon; + } + }); + } + + private void markRefsAdvertised() { + for (final Ref r : getRefs()) { + markAdvertised(r.getObjectId()); + if (r.getPeeledObjectId() != null) + markAdvertised(r.getPeeledObjectId()); + } + } + + private void markAdvertised(final AnyObjectId id) { + try { + walk.parseAny(id).add(ADVERTISED); + } catch (IOException readError) { + // We probably just do not have this object locally. + } + } + + private void markCommon(final RevObject obj) { + obj.add(COMMON); + if (obj instanceof RevCommit) + ((RevCommit) obj).carry(COMMON); + } + + private void receivePack(final ProgressMonitor monitor) throws IOException { + final IndexPack ip; + + ip = IndexPack.create(local, sideband ? pckIn.sideband(monitor) : in); + ip.setFixThin(thinPack); + ip.setObjectChecking(transport.isCheckFetchedObjects()); + ip.index(monitor); + packLock = ip.renameAndOpenPack(lockMessage); + } + + private static class CancelledException extends Exception { + private static final long serialVersionUID = 1L; + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java new file mode 100644 index 0000000000..b1ce28d35f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +import org.eclipse.jgit.errors.NoRemoteRepositoryException; +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.PackProtocolException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PackWriter; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.transport.RemoteRefUpdate.Status; + +/** + * Push implementation using the native Git pack transfer service. + * <p> + * This is the canonical implementation for transferring objects to the remote + * repository from the local repository by talking to the 'git-receive-pack' + * service. Objects are packed on the local side into a pack file and then sent + * to the remote repository. + * <p> + * This connection requires only a bi-directional pipe or socket, and thus is + * easily wrapped up into a local process pipe, anonymous TCP socket, or a + * command executed through an SSH tunnel. + * <p> + * This implementation honors {@link Transport#isPushThin()} option. + * <p> + * Concrete implementations should just call + * {@link #init(java.io.InputStream, java.io.OutputStream)} and + * {@link #readAdvertisedRefs()} methods in constructor or before any use. They + * should also handle resources releasing in {@link #close()} method if needed. + */ +class BasePackPushConnection extends BasePackConnection implements + PushConnection { + static final String CAPABILITY_REPORT_STATUS = "report-status"; + + static final String CAPABILITY_DELETE_REFS = "delete-refs"; + + static final String CAPABILITY_OFS_DELTA = "ofs-delta"; + + private final boolean thinPack; + + private boolean capableDeleteRefs; + + private boolean capableReport; + + private boolean capableOfsDelta; + + private boolean sentCommand; + + private boolean writePack; + + /** Time in milliseconds spent transferring the pack data. */ + private long packTransferTime; + + BasePackPushConnection(final PackTransport packTransport) { + super(packTransport); + thinPack = transport.isPushThin(); + } + + public void push(final ProgressMonitor monitor, + final Map<String, RemoteRefUpdate> refUpdates) + throws TransportException { + markStartedOperation(); + doPush(monitor, refUpdates); + } + + @Override + protected TransportException noRepository() { + // Sadly we cannot tell the "invalid URI" case from "push not allowed". + // Opening a fetch connection can help us tell the difference, as any + // useful repository is going to support fetch if it also would allow + // push. So if fetch throws NoRemoteRepositoryException we know the + // URI is wrong. Otherwise we can correctly state push isn't allowed + // as the fetch connection opened successfully. + // + try { + transport.openFetch().close(); + } catch (NotSupportedException e) { + // Fall through. + } catch (NoRemoteRepositoryException e) { + // Fetch concluded the repository doesn't exist. + // + return e; + } catch (TransportException e) { + // Fall through. + } + return new TransportException(uri, "push not permitted"); + } + + protected void doPush(final ProgressMonitor monitor, + final Map<String, RemoteRefUpdate> refUpdates) + throws TransportException { + try { + writeCommands(refUpdates.values(), monitor); + if (writePack) + writePack(refUpdates, monitor); + if (sentCommand && capableReport) + readStatusReport(refUpdates); + } catch (TransportException e) { + throw e; + } catch (Exception e) { + throw new TransportException(uri, e.getMessage(), e); + } finally { + close(); + } + } + + private void writeCommands(final Collection<RemoteRefUpdate> refUpdates, + final ProgressMonitor monitor) throws IOException { + final String capabilities = enableCapabilities(); + for (final RemoteRefUpdate rru : refUpdates) { + if (!capableDeleteRefs && rru.isDelete()) { + rru.setStatus(Status.REJECTED_NODELETE); + continue; + } + + final StringBuilder sb = new StringBuilder(); + final Ref advertisedRef = getRef(rru.getRemoteName()); + final ObjectId oldId = (advertisedRef == null ? ObjectId.zeroId() + : advertisedRef.getObjectId()); + sb.append(oldId.name()); + sb.append(' '); + sb.append(rru.getNewObjectId().name()); + sb.append(' '); + sb.append(rru.getRemoteName()); + if (!sentCommand) { + sentCommand = true; + sb.append(capabilities); + } + + pckOut.writeString(sb.toString()); + rru.setStatus(sentCommand ? Status.AWAITING_REPORT : Status.OK); + if (!rru.isDelete()) + writePack = true; + } + + if (monitor.isCancelled()) + throw new TransportException(uri, "push cancelled"); + pckOut.end(); + outNeedsEnd = false; + } + + private String enableCapabilities() { + final StringBuilder line = new StringBuilder(); + capableReport = wantCapability(line, CAPABILITY_REPORT_STATUS); + capableDeleteRefs = wantCapability(line, CAPABILITY_DELETE_REFS); + capableOfsDelta = wantCapability(line, CAPABILITY_OFS_DELTA); + if (line.length() > 0) + line.setCharAt(0, '\0'); + return line.toString(); + } + + private void writePack(final Map<String, RemoteRefUpdate> refUpdates, + final ProgressMonitor monitor) throws IOException { + final PackWriter writer = new PackWriter(local, monitor); + final ArrayList<ObjectId> remoteObjects = new ArrayList<ObjectId>( + getRefs().size()); + final ArrayList<ObjectId> newObjects = new ArrayList<ObjectId>( + refUpdates.size()); + + for (final Ref r : getRefs()) + remoteObjects.add(r.getObjectId()); + remoteObjects.addAll(additionalHaves); + for (final RemoteRefUpdate r : refUpdates.values()) { + if (!ObjectId.zeroId().equals(r.getNewObjectId())) + newObjects.add(r.getNewObjectId()); + } + + writer.setThin(thinPack); + writer.setDeltaBaseAsOffset(capableOfsDelta); + writer.preparePack(newObjects, remoteObjects); + final long start = System.currentTimeMillis(); + writer.writePack(out); + packTransferTime = System.currentTimeMillis() - start; + } + + private void readStatusReport(final Map<String, RemoteRefUpdate> refUpdates) + throws IOException { + final String unpackLine = readStringLongTimeout(); + if (!unpackLine.startsWith("unpack ")) + throw new PackProtocolException(uri, "unexpected report line: " + + unpackLine); + final String unpackStatus = unpackLine.substring("unpack ".length()); + if (!unpackStatus.equals("ok")) + throw new TransportException(uri, + "error occurred during unpacking on the remote end: " + + unpackStatus); + + String refLine; + while ((refLine = pckIn.readString()) != PacketLineIn.END) { + boolean ok = false; + int refNameEnd = -1; + if (refLine.startsWith("ok ")) { + ok = true; + refNameEnd = refLine.length(); + } else if (refLine.startsWith("ng ")) { + ok = false; + refNameEnd = refLine.indexOf(" ", 3); + } + if (refNameEnd == -1) + throw new PackProtocolException(uri + + ": unexpected report line: " + refLine); + final String refName = refLine.substring(3, refNameEnd); + final String message = (ok ? null : refLine + .substring(refNameEnd + 1)); + + final RemoteRefUpdate rru = refUpdates.get(refName); + if (rru == null) + throw new PackProtocolException(uri + + ": unexpected ref report: " + refName); + if (ok) { + rru.setStatus(Status.OK); + } else { + rru.setStatus(Status.REJECTED_OTHER_REASON); + rru.setMessage(message); + } + } + for (final RemoteRefUpdate rru : refUpdates.values()) { + if (rru.getStatus() == Status.AWAITING_REPORT) + throw new PackProtocolException(uri + + ": expected report for ref " + rru.getRemoteName() + + " not received"); + } + } + + private String readStringLongTimeout() throws IOException { + if (timeoutIn == null) + return pckIn.readString(); + + // The remote side may need a lot of time to choke down the pack + // we just sent them. There may be many deltas that need to be + // resolved by the remote. Its hard to say how long the other + // end is going to be silent. Taking 10x the configured timeout + // or the time spent transferring the pack, whichever is larger, + // gives the other side some reasonable window to process the data, + // but this is just a wild guess. + // + final int oldTimeout = timeoutIn.getTimeout(); + final int sendTime = (int) Math.min(packTransferTime, 28800000L); + try { + timeoutIn.setTimeout(10 * Math.max(sendTime, oldTimeout)); + return pckIn.readString(); + } finally { + timeoutIn.setTimeout(oldTimeout); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java new file mode 100644 index 0000000000..a0d172e0fa --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2009, Matthias Sohn <matthias.sohn@sap.com> + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; + +import org.eclipse.jgit.errors.MissingBundlePrerequisiteException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.PackProtocolException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PackLock; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * Fetch connection for bundle based classes. It used by + * instances of {@link TransportBundle} + */ +class BundleFetchConnection extends BaseFetchConnection { + + private final Transport transport; + + InputStream bin; + + final Set<ObjectId> prereqs = new HashSet<ObjectId>(); + + private String lockMessage; + + private PackLock packLock; + + BundleFetchConnection(Transport transportBundle, final InputStream src) throws TransportException { + transport = transportBundle; + bin = new BufferedInputStream(src, IndexPack.BUFFER_SIZE); + try { + switch (readSignature()) { + case 2: + readBundleV2(); + break; + default: + throw new TransportException(transport.uri, "not a bundle"); + } + } catch (TransportException err) { + close(); + throw err; + } catch (IOException err) { + close(); + throw new TransportException(transport.uri, err.getMessage(), err); + } catch (RuntimeException err) { + close(); + throw new TransportException(transport.uri, err.getMessage(), err); + } + } + + private int readSignature() throws IOException { + final String rev = readLine(new byte[1024]); + if (TransportBundle.V2_BUNDLE_SIGNATURE.equals(rev)) + return 2; + throw new TransportException(transport.uri, "not a bundle"); + } + + private void readBundleV2() throws IOException { + final byte[] hdrbuf = new byte[1024]; + final LinkedHashMap<String, Ref> avail = new LinkedHashMap<String, Ref>(); + for (;;) { + String line = readLine(hdrbuf); + if (line.length() == 0) + break; + + if (line.charAt(0) == '-') { + prereqs.add(ObjectId.fromString(line.substring(1, 41))); + continue; + } + + final String name = line.substring(41, line.length()); + final ObjectId id = ObjectId.fromString(line.substring(0, 40)); + final Ref prior = avail.put(name, new Ref(Ref.Storage.NETWORK, + name, id)); + if (prior != null) + throw duplicateAdvertisement(name); + } + available(avail); + } + + private PackProtocolException duplicateAdvertisement(final String name) { + return new PackProtocolException(transport.uri, + "duplicate advertisements of " + name); + } + + private String readLine(final byte[] hdrbuf) throws IOException { + bin.mark(hdrbuf.length); + final int cnt = bin.read(hdrbuf); + int lf = 0; + while (lf < cnt && hdrbuf[lf] != '\n') + lf++; + bin.reset(); + NB.skipFully(bin, lf); + if (lf < cnt && hdrbuf[lf] == '\n') + NB.skipFully(bin, 1); + return RawParseUtils.decode(Constants.CHARSET, hdrbuf, 0, lf); + } + + public boolean didFetchTestConnectivity() { + return false; + } + + @Override + protected void doFetch(final ProgressMonitor monitor, + final Collection<Ref> want, final Set<ObjectId> have) + throws TransportException { + verifyPrerequisites(); + try { + final IndexPack ip = newIndexPack(); + ip.index(monitor); + packLock = ip.renameAndOpenPack(lockMessage); + } catch (IOException err) { + close(); + throw new TransportException(transport.uri, err.getMessage(), err); + } catch (RuntimeException err) { + close(); + throw new TransportException(transport.uri, err.getMessage(), err); + } + } + + public void setPackLockMessage(final String message) { + lockMessage = message; + } + + public Collection<PackLock> getPackLocks() { + if (packLock != null) + return Collections.singleton(packLock); + return Collections.<PackLock> emptyList(); + } + + private IndexPack newIndexPack() throws IOException { + final IndexPack ip = IndexPack.create(transport.local, bin); + ip.setFixThin(true); + ip.setObjectChecking(transport.isCheckFetchedObjects()); + return ip; + } + + private void verifyPrerequisites() throws TransportException { + if (prereqs.isEmpty()) + return; + + final RevWalk rw = new RevWalk(transport.local); + final RevFlag PREREQ = rw.newFlag("PREREQ"); + final RevFlag SEEN = rw.newFlag("SEEN"); + + final List<ObjectId> missing = new ArrayList<ObjectId>(); + final List<RevObject> commits = new ArrayList<RevObject>(); + for (final ObjectId p : prereqs) { + try { + final RevCommit c = rw.parseCommit(p); + if (!c.has(PREREQ)) { + c.add(PREREQ); + commits.add(c); + } + } catch (MissingObjectException notFound) { + missing.add(p); + } catch (IOException err) { + throw new TransportException(transport.uri, "Cannot read commit " + + p.name(), err); + } + } + if (!missing.isEmpty()) + throw new MissingBundlePrerequisiteException(transport.uri, missing); + + for (final Ref r : transport.local.getAllRefs().values()) { + try { + rw.markStart(rw.parseCommit(r.getObjectId())); + } catch (IOException readError) { + // If we cannot read the value of the ref skip it. + } + } + + int remaining = commits.size(); + try { + RevCommit c; + while ((c = rw.next()) != null) { + if (c.has(PREREQ)) { + c.add(SEEN); + if (--remaining == 0) + break; + } + } + } catch (IOException err) { + throw new TransportException(transport.uri, "Cannot read object", err); + } + + if (remaining > 0) { + for (final RevObject o : commits) { + if (!o.has(SEEN)) + missing.add(o); + } + throw new MissingBundlePrerequisiteException(transport.uri, missing); + } + } + + @Override + public void close() { + if (bin != null) { + try { + bin.close(); + } catch (IOException ie) { + // Ignore close failures. + } finally { + bin = null; + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleWriter.java new file mode 100644 index 0000000000..92d07e1283 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleWriter.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PackWriter; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; + +/** + * Creates a Git bundle file, for sneaker-net transport to another system. + * <p> + * Bundles generated by this class can be later read in from a file URI using + * the bundle transport, or from an application controlled buffer by the more + * generic {@link TransportBundleStream}. + * <p> + * Applications creating bundles need to call one or more <code>include</code> + * calls to reflect which objects should be available as refs in the bundle for + * the other side to fetch. At least one include is required to create a valid + * bundle file, and duplicate names are not permitted. + * <p> + * Optional <code>assume</code> calls can be made to declare commits which the + * recipient must have in order to fetch from the bundle file. Objects reachable + * from these assumed commits can be used as delta bases in order to reduce the + * overall bundle size. + */ +public class BundleWriter { + private final PackWriter packWriter; + + private final Map<String, ObjectId> include; + + private final Set<RevCommit> assume; + + /** + * Create a writer for a bundle. + * + * @param repo + * repository where objects are stored. + * @param monitor + * operations progress monitor. + */ + public BundleWriter(final Repository repo, final ProgressMonitor monitor) { + packWriter = new PackWriter(repo, monitor); + include = new TreeMap<String, ObjectId>(); + assume = new HashSet<RevCommit>(); + } + + /** + * Include an object (and everything reachable from it) in the bundle. + * + * @param name + * name the recipient can discover this object as from the + * bundle's list of advertised refs . The name must be a valid + * ref format and must not have already been included in this + * bundle writer. + * @param id + * object to pack. Multiple refs may point to the same object. + */ + public void include(final String name, final AnyObjectId id) { + if (!Repository.isValidRefName(name)) + throw new IllegalArgumentException("Invalid ref name: " + name); + if (include.containsKey(name)) + throw new IllegalStateException("Duplicate ref: " + name); + include.put(name, id.toObjectId()); + } + + /** + * Include a single ref (a name/object pair) in the bundle. + * <p> + * This is a utility function for: + * <code>include(r.getName(), r.getObjectId())</code>. + * + * @param r + * the ref to include. + */ + public void include(final Ref r) { + include(r.getName(), r.getObjectId()); + } + + /** + * Assume a commit is available on the recipient's side. + * <p> + * In order to fetch from a bundle the recipient must have any assumed + * commit. Each assumed commit is explicitly recorded in the bundle header + * to permit the recipient to validate it has these objects. + * + * @param c + * the commit to assume being available. This commit should be + * parsed and not disposed in order to maximize the amount of + * debugging information available in the bundle stream. + */ + public void assume(final RevCommit c) { + if (c != null) + assume.add(c); + } + + /** + * Generate and write the bundle to the output stream. + * <p> + * This method can only be called once per BundleWriter instance. + * + * @param os + * the stream the bundle is written to. If the stream is not + * buffered it will be buffered by the writer. Caller is + * responsible for closing the stream. + * @throws IOException + * an error occurred reading a local object's data to include in + * the bundle, or writing compressed object data to the output + * stream. + */ + public void writeBundle(OutputStream os) throws IOException { + if (!(os instanceof BufferedOutputStream)) + os = new BufferedOutputStream(os); + + final HashSet<ObjectId> inc = new HashSet<ObjectId>(); + final HashSet<ObjectId> exc = new HashSet<ObjectId>(); + inc.addAll(include.values()); + for (final RevCommit r : assume) + exc.add(r.getId()); + packWriter.setThin(exc.size() > 0); + packWriter.preparePack(inc, exc); + + final Writer w = new OutputStreamWriter(os, Constants.CHARSET); + w.write(TransportBundle.V2_BUNDLE_SIGNATURE); + w.write('\n'); + + final char[] tmp = new char[Constants.OBJECT_ID_LENGTH * 2]; + for (final RevCommit a : assume) { + w.write('-'); + a.copyTo(tmp, w); + if (a.getRawBuffer() != null) { + w.write(' '); + w.write(a.getShortMessage()); + } + w.write('\n'); + } + for (final Map.Entry<String, ObjectId> e : include.entrySet()) { + e.getValue().copyTo(tmp, w); + w.write(' '); + w.write(e.getKey()); + w.write('\n'); + } + + w.write('\n'); + w.flush(); + packWriter.writePack(os); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java new file mode 100644 index 0000000000..fbed0693ca --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.util.Collection; +import java.util.Map; + +import org.eclipse.jgit.lib.Ref; + +/** + * Represent connection for operation on a remote repository. + * <p> + * Currently all operations on remote repository (fetch and push) provide + * information about remote refs. Every connection is able to be closed and + * should be closed - this is a connection client responsibility. + * + * @see Transport + */ +public interface Connection { + + /** + * Get the complete map of refs advertised as available for fetching or + * pushing. + * + * @return available/advertised refs: map of refname to ref. Never null. Not + * modifiable. The collection can be empty if the remote side has no + * refs (it is an empty/newly created repository). + */ + public Map<String, Ref> getRefsMap(); + + /** + * Get the complete list of refs advertised as available for fetching or + * pushing. + * <p> + * The returned refs may appear in any order. If the caller needs these to + * be sorted, they should be copied into a new array or List and then sorted + * by the caller as necessary. + * + * @return available/advertised refs. Never null. Not modifiable. The + * collection can be empty if the remote side has no refs (it is an + * empty/newly created repository). + */ + public Collection<Ref> getRefs(); + + /** + * Get a single advertised ref by name. + * <p> + * The name supplied should be valid ref name. To get a peeled value for a + * ref (aka <code>refs/tags/v1.0^{}</code>) use the base name (without + * the <code>^{}</code> suffix) and look at the peeled object id. + * + * @param name + * name of the ref to obtain. + * @return the requested ref; null if the remote did not advertise this ref. + */ + public Ref getRef(final String name); + + /** + * Close any resources used by this connection. + * <p> + * If the remote repository is contacted by a network socket this method + * must close that network socket, disconnecting the two peers. If the + * remote repository is actually local (same system) this method must close + * any open file handles used to read the "remote" repository. + */ + public void close(); + +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java new file mode 100644 index 0000000000..c6f69043be --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryCache; +import org.eclipse.jgit.lib.RepositoryCache.FileKey; + +/** Basic daemon for the anonymous <code>git://</code> transport protocol. */ +public class Daemon { + /** 9418: IANA assigned port number for Git. */ + public static final int DEFAULT_PORT = 9418; + + private static final int BACKLOG = 5; + + private InetSocketAddress myAddress; + + private final DaemonService[] services; + + private final ThreadGroup processors; + + private volatile boolean exportAll; + + private Map<String, Repository> exports; + + private Collection<File> exportBase; + + private boolean run; + + private Thread acceptThread; + + private int timeout; + + /** Configure a daemon to listen on any available network port. */ + public Daemon() { + this(null); + } + + /** + * Configure a new daemon for the specified network address. + * + * @param addr + * address to listen for connections on. If null, any available + * port will be chosen on all network interfaces. + */ + public Daemon(final InetSocketAddress addr) { + myAddress = addr; + exports = new ConcurrentHashMap<String, Repository>(); + exportBase = new CopyOnWriteArrayList<File>(); + processors = new ThreadGroup("Git-Daemon"); + + services = new DaemonService[] { + new DaemonService("upload-pack", "uploadpack") { + { + setEnabled(true); + } + + @Override + protected void execute(final DaemonClient dc, + final Repository db) throws IOException { + final UploadPack rp = new UploadPack(db); + final InputStream in = dc.getInputStream(); + rp.setTimeout(Daemon.this.getTimeout()); + rp.upload(in, dc.getOutputStream(), null); + } + }, new DaemonService("receive-pack", "receivepack") { + { + setEnabled(false); + } + + @Override + protected void execute(final DaemonClient dc, + final Repository db) throws IOException { + final InetAddress peer = dc.getRemoteAddress(); + String host = peer.getCanonicalHostName(); + if (host == null) + host = peer.getHostAddress(); + final ReceivePack rp = new ReceivePack(db); + final InputStream in = dc.getInputStream(); + final String name = "anonymous"; + final String email = name + "@" + host; + rp.setRefLogIdent(new PersonIdent(name, email)); + rp.setTimeout(Daemon.this.getTimeout()); + rp.receive(in, dc.getOutputStream(), null); + } + } }; + } + + /** @return the address connections are received on. */ + public synchronized InetSocketAddress getAddress() { + return myAddress; + } + + /** + * Lookup a supported service so it can be reconfigured. + * + * @param name + * name of the service; e.g. "receive-pack"/"git-receive-pack" or + * "upload-pack"/"git-upload-pack". + * @return the service; null if this daemon implementation doesn't support + * the requested service type. + */ + public synchronized DaemonService getService(String name) { + if (!name.startsWith("git-")) + name = "git-" + name; + for (final DaemonService s : services) { + if (s.getCommandName().equals(name)) + return s; + } + return null; + } + + /** + * @return false if <code>git-daemon-export-ok</code> is required to export + * a repository; true if <code>git-daemon-export-ok</code> is + * ignored. + * @see #setExportAll(boolean) + */ + public boolean isExportAll() { + return exportAll; + } + + /** + * Set whether or not to export all repositories. + * <p> + * If false (the default), repositories must have a + * <code>git-daemon-export-ok</code> file to be accessed through this + * daemon. + * <p> + * If true, all repositories are available through the daemon, whether or + * not <code>git-daemon-export-ok</code> exists. + * + * @param export + */ + public void setExportAll(final boolean export) { + exportAll = export; + } + + /** + * Add a single repository to the set that is exported by this daemon. + * <p> + * The existence (or lack-thereof) of <code>git-daemon-export-ok</code> is + * ignored by this method. The repository is always published. + * + * @param name + * name the repository will be published under. + * @param db + * the repository instance. + */ + public void exportRepository(String name, final Repository db) { + if (!name.endsWith(".git")) + name = name + ".git"; + exports.put(name, db); + RepositoryCache.register(db); + } + + /** + * Recursively export all Git repositories within a directory. + * + * @param dir + * the directory to export. This directory must not itself be a + * git repository, but any directory below it which has a file + * named <code>git-daemon-export-ok</code> will be published. + */ + public void exportDirectory(final File dir) { + exportBase.add(dir); + } + + /** @return timeout (in seconds) before aborting an IO operation. */ + public int getTimeout() { + return timeout; + } + + /** + * Set the timeout before willing to abort an IO call. + * + * @param seconds + * number of seconds to wait (with no data transfer occurring) + * before aborting an IO read or write operation with the + * connected client. + */ + public void setTimeout(final int seconds) { + timeout = seconds; + } + + /** + * Start this daemon on a background thread. + * + * @throws IOException + * the server socket could not be opened. + * @throws IllegalStateException + * the daemon is already running. + */ + public synchronized void start() throws IOException { + if (acceptThread != null) + throw new IllegalStateException("Daemon already running"); + + final ServerSocket listenSock = new ServerSocket( + myAddress != null ? myAddress.getPort() : 0, BACKLOG, + myAddress != null ? myAddress.getAddress() : null); + myAddress = (InetSocketAddress) listenSock.getLocalSocketAddress(); + + run = true; + acceptThread = new Thread(processors, "Git-Daemon-Accept") { + public void run() { + while (isRunning()) { + try { + startClient(listenSock.accept()); + } catch (InterruptedIOException e) { + // Test again to see if we should keep accepting. + } catch (IOException e) { + break; + } + } + + try { + listenSock.close(); + } catch (IOException err) { + // + } finally { + synchronized (Daemon.this) { + acceptThread = null; + } + } + } + }; + acceptThread.start(); + } + + /** @return true if this daemon is receiving connections. */ + public synchronized boolean isRunning() { + return run; + } + + /** Stop this daemon. */ + public synchronized void stop() { + if (acceptThread != null) { + run = false; + acceptThread.interrupt(); + } + } + + private void startClient(final Socket s) { + final DaemonClient dc = new DaemonClient(this); + + final SocketAddress peer = s.getRemoteSocketAddress(); + if (peer instanceof InetSocketAddress) + dc.setRemoteAddress(((InetSocketAddress) peer).getAddress()); + + new Thread(processors, "Git-Daemon-Client " + peer.toString()) { + public void run() { + try { + dc.execute(s); + } catch (IOException e) { + // Ignore unexpected IO exceptions from clients + e.printStackTrace(); + } finally { + try { + s.getInputStream().close(); + } catch (IOException e) { + // Ignore close exceptions + } + try { + s.getOutputStream().close(); + } catch (IOException e) { + // Ignore close exceptions + } + } + } + }.start(); + } + + synchronized DaemonService matchService(final String cmd) { + for (final DaemonService d : services) { + if (d.handles(cmd)) + return d; + } + return null; + } + + Repository openRepository(String name) { + // Assume any attempt to use \ was by a Windows client + // and correct to the more typical / used in Git URIs. + // + name = name.replace('\\', '/'); + + // git://thishost/path should always be name="/path" here + // + if (!name.startsWith("/")) + return null; + + // Forbid Windows UNC paths as they might escape the base + // + if (name.startsWith("//")) + return null; + + // Forbid funny paths which contain an up-reference, they + // might be trying to escape and read /../etc/password. + // + if (name.contains("/../")) + return null; + name = name.substring(1); + + Repository db; + db = exports.get(name.endsWith(".git") ? name : name + ".git"); + if (db != null) { + db.incrementOpen(); + return db; + } + + for (final File baseDir : exportBase) { + final File gitdir = FileKey.resolve(new File(baseDir, name)); + if (gitdir != null && canExport(gitdir)) + return openRepository(gitdir); + } + return null; + } + + private static Repository openRepository(final File gitdir) { + try { + return RepositoryCache.open(FileKey.exact(gitdir)); + } catch (IOException err) { + // null signals it "wasn't found", which is all that is suitable + // for the remote client to know. + return null; + } + } + + private boolean canExport(final File d) { + if (isExportAll()) { + return true; + } + return new File(d, "git-daemon-export-ok").exists(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/DaemonClient.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/DaemonClient.java new file mode 100644 index 0000000000..0b8de03439 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/DaemonClient.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; + +/** Active network client of {@link Daemon}. */ +public class DaemonClient { + private final Daemon daemon; + + private InetAddress peer; + + private InputStream rawIn; + + private OutputStream rawOut; + + DaemonClient(final Daemon d) { + daemon = d; + } + + void setRemoteAddress(final InetAddress ia) { + peer = ia; + } + + /** @return the daemon which spawned this client. */ + public Daemon getDaemon() { + return daemon; + } + + /** @return Internet address of the remote client. */ + public InetAddress getRemoteAddress() { + return peer; + } + + /** @return input stream to read from the connected client. */ + public InputStream getInputStream() { + return rawIn; + } + + /** @return output stream to send data to the connected client. */ + public OutputStream getOutputStream() { + return rawOut; + } + + void execute(final Socket sock) + throws IOException { + rawIn = new BufferedInputStream(sock.getInputStream()); + rawOut = new BufferedOutputStream(sock.getOutputStream()); + + if (0 < daemon.getTimeout()) + sock.setSoTimeout(daemon.getTimeout() * 1000); + String cmd = new PacketLineIn(rawIn).readStringRaw(); + final int nul = cmd.indexOf('\0'); + if (nul >= 0) { + // Newer clients hide a "host" header behind this byte. + // Currently we don't use it for anything, so we ignore + // this portion of the command. + // + cmd = cmd.substring(0, nul); + } + + final DaemonService srv = getDaemon().matchService(cmd); + if (srv == null) + return; + sock.setSoTimeout(0); + srv.execute(this, cmd); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/DaemonService.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/DaemonService.java new file mode 100644 index 0000000000..2b94957983 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/DaemonService.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; + +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Config.SectionParser; + +/** A service exposed by {@link Daemon} over anonymous <code>git://</code>. */ +public abstract class DaemonService { + private final String command; + + private final SectionParser<ServiceConfig> configKey; + + private boolean enabled; + + private boolean overridable; + + DaemonService(final String cmdName, final String cfgName) { + command = cmdName.startsWith("git-") ? cmdName : "git-" + cmdName; + configKey = new SectionParser<ServiceConfig>() { + public ServiceConfig parse(final Config cfg) { + return new ServiceConfig(DaemonService.this, cfg, cfgName); + } + }; + overridable = true; + } + + private static class ServiceConfig { + final boolean enabled; + + ServiceConfig(final DaemonService service, final Config cfg, + final String name) { + enabled = cfg.getBoolean("daemon", name, service.isEnabled()); + } + } + + /** @return is this service enabled for invocation? */ + public boolean isEnabled() { + return enabled; + } + + /** + * @param on + * true to allow this service to be used; false to deny it. + */ + public void setEnabled(final boolean on) { + enabled = on; + } + + /** @return can this service be configured in the repository config file? */ + public boolean isOverridable() { + return overridable; + } + + /** + * @param on + * true to permit repositories to override this service's enabled + * state with the <code>daemon.servicename</code> config setting. + */ + public void setOverridable(final boolean on) { + overridable = on; + } + + /** @return name of the command requested by clients. */ + public String getCommandName() { + return command; + } + + /** + * Determine if this service can handle the requested command. + * + * @param commandLine + * input line from the client. + * @return true if this command can accept the given command line. + */ + public boolean handles(final String commandLine) { + return command.length() + 1 < commandLine.length() + && commandLine.charAt(command.length()) == ' ' + && commandLine.startsWith(command); + } + + void execute(final DaemonClient client, final String commandLine) + throws IOException { + final String name = commandLine.substring(command.length() + 1); + final Repository db = client.getDaemon().openRepository(name); + if (db == null) + return; + try { + if (isEnabledFor(db)) + execute(client, db); + } finally { + db.close(); + } + } + + private boolean isEnabledFor(final Repository db) { + if (isOverridable()) + return db.getConfig().get(configKey).enabled; + return isEnabled(); + } + + abstract void execute(DaemonClient client, Repository db) + throws IOException; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/DefaultSshSessionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/DefaultSshSessionFactory.java new file mode 100644 index 0000000000..6ff3d4b2f3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/DefaultSshSessionFactory.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.awt.Container; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; + +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.JTextField; + +import com.jcraft.jsch.Session; +import com.jcraft.jsch.UIKeyboardInteractive; +import com.jcraft.jsch.UserInfo; + +/** + * Loads known hosts and private keys from <code>$HOME/.ssh</code>. + * <p> + * This is the default implementation used by JGit and provides most of the + * compatibility necessary to match OpenSSH, a popular implementation of SSH + * used by C Git. + * <p> + * If user interactivity is required by SSH (e.g. to obtain a password) AWT is + * used to display a password input field to the end-user. + */ +class DefaultSshSessionFactory extends SshConfigSessionFactory { + protected void configure(final OpenSshConfig.Host hc, final Session session) { + if (!hc.isBatchMode()) + session.setUserInfo(new AWT_UserInfo()); + } + + private static class AWT_UserInfo implements UserInfo, + UIKeyboardInteractive { + private String passwd; + + private String passphrase; + + public void showMessage(final String msg) { + JOptionPane.showMessageDialog(null, msg); + } + + public boolean promptYesNo(final String msg) { + return JOptionPane.showConfirmDialog(null, msg, "Warning", + JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION; + } + + public boolean promptPassword(final String msg) { + passwd = null; + final JPasswordField passwordField = new JPasswordField(20); + final int result = JOptionPane.showConfirmDialog(null, + new Object[] { passwordField }, msg, + JOptionPane.OK_CANCEL_OPTION); + if (result == JOptionPane.OK_OPTION) { + passwd = new String(passwordField.getPassword()); + return true; + } + return false; + } + + public boolean promptPassphrase(final String msg) { + passphrase = null; + final JPasswordField passwordField = new JPasswordField(20); + final int result = JOptionPane.showConfirmDialog(null, + new Object[] { passwordField }, msg, + JOptionPane.OK_CANCEL_OPTION); + if (result == JOptionPane.OK_OPTION) { + passphrase = new String(passwordField.getPassword()); + return true; + } + return false; + } + + public String getPassword() { + return passwd; + } + + public String getPassphrase() { + return passphrase; + } + + public String[] promptKeyboardInteractive(final String destination, + final String name, final String instruction, + final String[] prompt, final boolean[] echo) { + final GridBagConstraints gbc = new GridBagConstraints(0, 0, 1, 1, + 1, 1, GridBagConstraints.NORTHWEST, + GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0); + final Container panel = new JPanel(); + panel.setLayout(new GridBagLayout()); + + gbc.weightx = 1.0; + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.gridx = 0; + panel.add(new JLabel(instruction), gbc); + gbc.gridy++; + + gbc.gridwidth = GridBagConstraints.RELATIVE; + + final JTextField[] texts = new JTextField[prompt.length]; + for (int i = 0; i < prompt.length; i++) { + gbc.fill = GridBagConstraints.NONE; + gbc.gridx = 0; + gbc.weightx = 1; + panel.add(new JLabel(prompt[i]), gbc); + + gbc.gridx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weighty = 1; + if (echo[i]) { + texts[i] = new JTextField(20); + } else { + texts[i] = new JPasswordField(20); + } + panel.add(texts[i], gbc); + gbc.gridy++; + } + + if (JOptionPane.showConfirmDialog(null, panel, destination + ": " + + name, JOptionPane.OK_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE) == JOptionPane.OK_OPTION) { + String[] response = new String[prompt.length]; + for (int i = 0; i < prompt.length; i++) { + response[i] = texts[i].getText(); + } + return response; + } + return null; // cancel + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchConnection.java new file mode 100644 index 0000000000..dea0f2dc17 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchConnection.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Mike Ralphson <mike@abacus.co.uk> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.util.Collection; +import java.util.Set; + +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PackLock; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; + +/** + * Lists known refs from the remote and copies objects of selected refs. + * <p> + * A fetch connection typically connects to the <code>git-upload-pack</code> + * service running where the remote repository is stored. This provides a + * one-way object transfer service to copy objects from the remote repository + * into this local repository. + * <p> + * Instances of a FetchConnection must be created by a {@link Transport} that + * implements a specific object transfer protocol that both sides of the + * connection understand. + * <p> + * FetchConnection instances are not thread safe and may be accessed by only one + * thread at a time. + * + * @see Transport + */ +public interface FetchConnection extends Connection { + /** + * Fetch objects we don't have but that are reachable from advertised refs. + * <p> + * Only one call per connection is allowed. Subsequent calls will result in + * {@link TransportException}. + * </p> + * <p> + * Implementations are free to use network connections as necessary to + * efficiently (for both client and server) transfer objects from the remote + * repository into this repository. When possible implementations should + * avoid replacing/overwriting/duplicating an object already available in + * the local destination repository. Locally available objects and packs + * should always be preferred over remotely available objects and packs. + * {@link Transport#isFetchThin()} should be honored if applicable. + * </p> + * + * @param monitor + * progress monitor to inform the end-user about the amount of + * work completed, or to indicate cancellation. Implementations + * should poll the monitor at regular intervals to look for + * cancellation requests from the user. + * @param want + * one or more refs advertised by this connection that the caller + * wants to store locally. + * @param have + * additional objects known to exist in the destination + * repository, especially if they aren't yet reachable by the ref + * database. Connections should take this set as an addition to + * what is reachable through all Refs, not in replace of it. + * @throws TransportException + * objects could not be copied due to a network failure, + * protocol error, or error on remote side, or connection was + * already used for fetch. + */ + public void fetch(final ProgressMonitor monitor, + final Collection<Ref> want, final Set<ObjectId> have) + throws TransportException; + + /** + * Did the last {@link #fetch(ProgressMonitor, Collection, Set)} get tags? + * <p> + * Some Git aware transports are able to implicitly grab an annotated tag if + * {@link TagOpt#AUTO_FOLLOW} or {@link TagOpt#FETCH_TAGS} was selected and + * the object the tag peels to (references) was transferred as part of the + * last {@link #fetch(ProgressMonitor, Collection, Set)} call. If it is + * possible for such tags to have been included in the transfer this method + * returns true, allowing the caller to attempt tag discovery. + * <p> + * By returning only true/false (and not the actual list of tags obtained) + * the transport itself does not need to be aware of whether or not tags + * were included in the transfer. + * + * @return true if the last fetch call implicitly included tag objects; + * false if tags were not implicitly obtained. + */ + public boolean didFetchIncludeTags(); + + /** + * Did the last {@link #fetch(ProgressMonitor, Collection, Set)} validate + * graph? + * <p> + * Some transports walk the object graph on the client side, with the client + * looking for what objects it is missing and requesting them individually + * from the remote peer. By virtue of completing the fetch call the client + * implicitly tested the object connectivity, as every object in the graph + * was either already local or was requested successfully from the peer. In + * such transports this method returns true. + * <p> + * Some transports assume the remote peer knows the Git object graph and is + * able to supply a fully connected graph to the client (although it may + * only be transferring the parts the client does not yet have). Its faster + * to assume such remote peers are well behaved and send the correct + * response to the client. In such transports this method returns false. + * + * @return true if the last fetch had to perform a connectivity check on the + * client side in order to succeed; false if the last fetch assumed + * the remote peer supplied a complete graph. + */ + public boolean didFetchTestConnectivity(); + + /** + * Set the lock message used when holding a pack out of garbage collection. + * <p> + * Callers that set a lock message <b>must</b> ensure they call + * {@link #getPackLocks()} after + * {@link #fetch(ProgressMonitor, Collection, Set)}, even if an exception + * was thrown, and release the locks that are held. + * + * @param message message to use when holding a pack in place. + */ + public void setPackLockMessage(String message); + + /** + * All locks created by the last + * {@link #fetch(ProgressMonitor, Collection, Set)} call. + * + * @return collection (possibly empty) of locks created by the last call to + * fetch. The caller must release these after refs are updated in + * order to safely permit garbage collection. + */ + public Collection<PackLock> getPackLocks(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchHeadRecord.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchHeadRecord.java new file mode 100644 index 0000000000..e2ec71030c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchHeadRecord.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2008, Charles O'Farrell <charleso@charleso.org> + * Copyright (C) 2009, Robin Rosenberg <robin.rosenberg@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import static org.eclipse.jgit.lib.Constants.R_HEADS; +import static org.eclipse.jgit.lib.Constants.R_REMOTES; +import static org.eclipse.jgit.lib.Constants.R_TAGS; + +import java.io.IOException; +import java.io.Writer; + +import org.eclipse.jgit.lib.ObjectId; + +class FetchHeadRecord { + ObjectId newValue; + + boolean notForMerge; + + String sourceName; + + URIish sourceURI; + + void write(final Writer pw) throws IOException { + final String type; + final String name; + if (sourceName.startsWith(R_HEADS)) { + type = "branch"; + name = sourceName.substring(R_HEADS.length()); + } else if (sourceName.startsWith(R_TAGS)) { + type = "tag"; + name = sourceName.substring(R_TAGS.length()); + } else if (sourceName.startsWith(R_REMOTES)) { + type = "remote branch"; + name = sourceName.substring(R_REMOTES.length()); + } else { + type = ""; + name = sourceName; + } + + pw.write(newValue.name()); + pw.write('\t'); + if (notForMerge) + pw.write("not-for-merge"); + pw.write('\t'); + pw.write(type); + pw.write(" '"); + pw.write(name); + pw.write("' of "); + pw.write(sourceURI.toString()); + pw.write("\n"); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java new file mode 100644 index 0000000000..65a5b1769e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.LockFile; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PackLock; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.ObjectWalk; +import org.eclipse.jgit.revwalk.RevWalk; + +class FetchProcess { + /** Transport we will fetch over. */ + private final Transport transport; + + /** List of things we want to fetch from the remote repository. */ + private final Collection<RefSpec> toFetch; + + /** Set of refs we will actually wind up asking to obtain. */ + private final HashMap<ObjectId, Ref> askFor = new HashMap<ObjectId, Ref>(); + + /** Objects we know we have locally. */ + private final HashSet<ObjectId> have = new HashSet<ObjectId>(); + + /** Updates to local tracking branches (if any). */ + private final ArrayList<TrackingRefUpdate> localUpdates = new ArrayList<TrackingRefUpdate>(); + + /** Records to be recorded into FETCH_HEAD. */ + private final ArrayList<FetchHeadRecord> fetchHeadUpdates = new ArrayList<FetchHeadRecord>(); + + private final ArrayList<PackLock> packLocks = new ArrayList<PackLock>(); + + private FetchConnection conn; + + FetchProcess(final Transport t, final Collection<RefSpec> f) { + transport = t; + toFetch = f; + } + + void execute(final ProgressMonitor monitor, final FetchResult result) + throws NotSupportedException, TransportException { + askFor.clear(); + localUpdates.clear(); + fetchHeadUpdates.clear(); + packLocks.clear(); + + try { + executeImp(monitor, result); + } finally { + for (final PackLock lock : packLocks) + lock.unlock(); + } + } + + private void executeImp(final ProgressMonitor monitor, + final FetchResult result) throws NotSupportedException, + TransportException { + conn = transport.openFetch(); + try { + result.setAdvertisedRefs(transport.getURI(), conn.getRefsMap()); + final Set<Ref> matched = new HashSet<Ref>(); + for (final RefSpec spec : toFetch) { + if (spec.getSource() == null) + throw new TransportException( + "Source ref not specified for refspec: " + spec); + + if (spec.isWildcard()) + expandWildcard(spec, matched); + else + expandSingle(spec, matched); + } + + Collection<Ref> additionalTags = Collections.<Ref> emptyList(); + final TagOpt tagopt = transport.getTagOpt(); + if (tagopt == TagOpt.AUTO_FOLLOW) + additionalTags = expandAutoFollowTags(); + else if (tagopt == TagOpt.FETCH_TAGS) + expandFetchTags(); + + final boolean includedTags; + if (!askFor.isEmpty() && !askForIsComplete()) { + fetchObjects(monitor); + includedTags = conn.didFetchIncludeTags(); + + // Connection was used for object transfer. If we + // do another fetch we must open a new connection. + // + closeConnection(); + } else { + includedTags = false; + } + + if (tagopt == TagOpt.AUTO_FOLLOW && !additionalTags.isEmpty()) { + // There are more tags that we want to follow, but + // not all were asked for on the initial request. + // + have.addAll(askFor.keySet()); + askFor.clear(); + for (final Ref r : additionalTags) { + final ObjectId id = r.getPeeledObjectId(); + if (id == null || transport.local.hasObject(id)) + wantTag(r); + } + + if (!askFor.isEmpty() && (!includedTags || !askForIsComplete())) { + reopenConnection(); + if (!askFor.isEmpty()) + fetchObjects(monitor); + } + } + } finally { + closeConnection(); + } + + final RevWalk walk = new RevWalk(transport.local); + if (transport.isRemoveDeletedRefs()) + deleteStaleTrackingRefs(result, walk); + for (TrackingRefUpdate u : localUpdates) { + try { + u.update(walk); + result.add(u); + } catch (IOException err) { + throw new TransportException("Failure updating tracking ref " + + u.getLocalName() + ": " + err.getMessage(), err); + } + } + + if (!fetchHeadUpdates.isEmpty()) { + try { + updateFETCH_HEAD(result); + } catch (IOException err) { + throw new TransportException("Failure updating FETCH_HEAD: " + + err.getMessage(), err); + } + } + } + + private void fetchObjects(final ProgressMonitor monitor) + throws TransportException { + try { + conn.setPackLockMessage("jgit fetch " + transport.uri); + conn.fetch(monitor, askFor.values(), have); + } finally { + packLocks.addAll(conn.getPackLocks()); + } + if (transport.isCheckFetchedObjects() + && !conn.didFetchTestConnectivity() && !askForIsComplete()) + throw new TransportException(transport.getURI(), + "peer did not supply a complete object graph"); + } + + private void closeConnection() { + if (conn != null) { + conn.close(); + conn = null; + } + } + + private void reopenConnection() throws NotSupportedException, + TransportException { + if (conn != null) + return; + + conn = transport.openFetch(); + + // Since we opened a new connection we cannot be certain + // that the system we connected to has the same exact set + // of objects available (think round-robin DNS and mirrors + // that aren't updated at the same time). + // + // We rebuild our askFor list using only the refs that the + // new connection has offered to us. + // + final HashMap<ObjectId, Ref> avail = new HashMap<ObjectId, Ref>(); + for (final Ref r : conn.getRefs()) + avail.put(r.getObjectId(), r); + + final Collection<Ref> wants = new ArrayList<Ref>(askFor.values()); + askFor.clear(); + for (final Ref want : wants) { + final Ref newRef = avail.get(want.getObjectId()); + if (newRef != null) { + askFor.put(newRef.getObjectId(), newRef); + } else { + removeFetchHeadRecord(want.getObjectId()); + removeTrackingRefUpdate(want.getObjectId()); + } + } + } + + private void removeTrackingRefUpdate(final ObjectId want) { + final Iterator<TrackingRefUpdate> i = localUpdates.iterator(); + while (i.hasNext()) { + final TrackingRefUpdate u = i.next(); + if (u.getNewObjectId().equals(want)) + i.remove(); + } + } + + private void removeFetchHeadRecord(final ObjectId want) { + final Iterator<FetchHeadRecord> i = fetchHeadUpdates.iterator(); + while (i.hasNext()) { + final FetchHeadRecord fh = i.next(); + if (fh.newValue.equals(want)) + i.remove(); + } + } + + private void updateFETCH_HEAD(final FetchResult result) throws IOException { + final LockFile lock = new LockFile(new File(transport.local + .getDirectory(), "FETCH_HEAD")); + try { + if (lock.lock()) { + final Writer w = new OutputStreamWriter(lock.getOutputStream()); + try { + for (final FetchHeadRecord h : fetchHeadUpdates) { + h.write(w); + result.add(h); + } + } finally { + w.close(); + } + lock.commit(); + } + } finally { + lock.unlock(); + } + } + + private boolean askForIsComplete() throws TransportException { + try { + final ObjectWalk ow = new ObjectWalk(transport.local); + for (final ObjectId want : askFor.keySet()) + ow.markStart(ow.parseAny(want)); + for (final Ref ref : transport.local.getAllRefs().values()) + ow.markUninteresting(ow.parseAny(ref.getObjectId())); + ow.checkConnectivity(); + return true; + } catch (MissingObjectException e) { + return false; + } catch (IOException e) { + throw new TransportException("Unable to check connectivity.", e); + } + } + + private void expandWildcard(final RefSpec spec, final Set<Ref> matched) + throws TransportException { + for (final Ref src : conn.getRefs()) { + if (spec.matchSource(src) && matched.add(src)) + want(src, spec.expandFromSource(src)); + } + } + + private void expandSingle(final RefSpec spec, final Set<Ref> matched) + throws TransportException { + final Ref src = conn.getRef(spec.getSource()); + if (src == null) { + throw new TransportException("Remote does not have " + + spec.getSource() + " available for fetch."); + } + if (matched.add(src)) + want(src, spec); + } + + private Collection<Ref> expandAutoFollowTags() throws TransportException { + final Collection<Ref> additionalTags = new ArrayList<Ref>(); + final Map<String, Ref> haveRefs = transport.local.getAllRefs(); + for (final Ref r : conn.getRefs()) { + if (!isTag(r)) + continue; + if (r.getPeeledObjectId() == null) { + additionalTags.add(r); + continue; + } + + final Ref local = haveRefs.get(r.getName()); + if (local != null) { + if (!r.getObjectId().equals(local.getObjectId())) + wantTag(r); + } else if (askFor.containsKey(r.getPeeledObjectId()) + || transport.local.hasObject(r.getPeeledObjectId())) + wantTag(r); + else + additionalTags.add(r); + } + return additionalTags; + } + + private void expandFetchTags() throws TransportException { + final Map<String, Ref> haveRefs = transport.local.getAllRefs(); + for (final Ref r : conn.getRefs()) { + if (!isTag(r)) + continue; + final Ref local = haveRefs.get(r.getName()); + if (local == null || !r.getObjectId().equals(local.getObjectId())) + wantTag(r); + } + } + + private void wantTag(final Ref r) throws TransportException { + want(r, new RefSpec().setSource(r.getName()) + .setDestination(r.getName())); + } + + private void want(final Ref src, final RefSpec spec) + throws TransportException { + final ObjectId newId = src.getObjectId(); + if (spec.getDestination() != null) { + try { + final TrackingRefUpdate tru = createUpdate(spec, newId); + if (newId.equals(tru.getOldObjectId())) + return; + localUpdates.add(tru); + } catch (IOException err) { + // Bad symbolic ref? That is the most likely cause. + // + throw new TransportException("Cannot resolve" + + " local tracking ref " + spec.getDestination() + + " for updating.", err); + } + } + + askFor.put(newId, src); + + final FetchHeadRecord fhr = new FetchHeadRecord(); + fhr.newValue = newId; + fhr.notForMerge = spec.getDestination() != null; + fhr.sourceName = src.getName(); + fhr.sourceURI = transport.getURI(); + fetchHeadUpdates.add(fhr); + } + + private TrackingRefUpdate createUpdate(final RefSpec spec, + final ObjectId newId) throws IOException { + return new TrackingRefUpdate(transport.local, spec, newId, "fetch"); + } + + private void deleteStaleTrackingRefs(final FetchResult result, + final RevWalk walk) throws TransportException { + final Repository db = transport.local; + for (final Ref ref : db.getAllRefs().values()) { + final String refname = ref.getName(); + for (final RefSpec spec : toFetch) { + if (spec.matchDestination(refname)) { + final RefSpec s = spec.expandFromDestination(refname); + if (result.getAdvertisedRef(s.getSource()) == null) { + deleteTrackingRef(result, db, walk, s, ref); + } + } + } + } + } + + private void deleteTrackingRef(final FetchResult result, + final Repository db, final RevWalk walk, final RefSpec spec, + final Ref localRef) throws TransportException { + final String name = localRef.getName(); + try { + final TrackingRefUpdate u = new TrackingRefUpdate(db, name, spec + .getSource(), true, ObjectId.zeroId(), "deleted"); + result.add(u); + if (transport.isDryRun()){ + return; + } + u.delete(walk); + switch (u.getResult()) { + case NEW: + case NO_CHANGE: + case FAST_FORWARD: + case FORCED: + break; + default: + throw new TransportException(transport.getURI(), + "Cannot delete stale tracking ref " + name + ": " + + u.getResult().name()); + } + } catch (IOException e) { + throw new TransportException(transport.getURI(), + "Cannot delete stale tracking ref " + name, e); + } + } + + private static boolean isTag(final Ref r) { + return isTag(r.getName()); + } + + private static boolean isTag(final String name) { + return name.startsWith(Constants.R_TAGS); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchResult.java new file mode 100644 index 0000000000..df3a1937a3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchResult.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Final status after a successful fetch from a remote repository. + * + * @see Transport#fetch(org.eclipse.jgit.lib.ProgressMonitor, Collection) + */ +public class FetchResult extends OperationResult { + private final List<FetchHeadRecord> forMerge; + + FetchResult() { + forMerge = new ArrayList<FetchHeadRecord>(); + } + + void add(final FetchHeadRecord r) { + if (!r.notForMerge) + forMerge.add(r); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpTransport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpTransport.java new file mode 100644 index 0000000000..3793a0abfb --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpTransport.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2009, JetBrains s.r.o. + * Copyright (C) 2009, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import org.eclipse.jgit.lib.Repository; + +/** + * The base class for transports that use HTTP as underlying protocol. This class + * allows customizing HTTP connection settings. + */ +public abstract class HttpTransport extends Transport { + /** + * Create a new transport instance. + * + * @param local + * the repository this instance will fetch into, or push out of. + * This must be the repository passed to + * {@link #open(Repository, URIish)}. + * @param uri + * the URI used to access the remote repository. This must be the + * URI passed to {@link #open(Repository, URIish)}. + */ + protected HttpTransport(Repository local, URIish uri) { + super(local, uri); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/IndexPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/IndexPack.java new file mode 100644 index 0000000000..f368648471 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/IndexPack.java @@ -0,0 +1,1107 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.EOFException; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.zip.CRC32; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.BinaryDelta; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.InflaterCache; +import org.eclipse.jgit.lib.MutableObjectId; +import org.eclipse.jgit.lib.ObjectChecker; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdSubclassMap; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.PackIndexWriter; +import org.eclipse.jgit.lib.PackLock; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.WindowCursor; +import org.eclipse.jgit.util.NB; + +/** Indexes Git pack files for local use. */ +public class IndexPack { + /** Progress message when reading raw data from the pack. */ + public static final String PROGRESS_DOWNLOAD = "Receiving objects"; + + /** Progress message when computing names of delta compressed objects. */ + public static final String PROGRESS_RESOLVE_DELTA = "Resolving deltas"; + + /** + * Size of the internal stream buffer. + * <p> + * If callers are going to be supplying IndexPack a BufferedInputStream they + * should use this buffer size as the size of the buffer for that + * BufferedInputStream, and any other its may be wrapping. This way the + * buffers will cascade efficiently and only the IndexPack buffer will be + * receiving the bulk of the data stream. + */ + public static final int BUFFER_SIZE = 8192; + + /** + * Create an index pack instance to load a new pack into a repository. + * <p> + * The received pack data and generated index will be saved to temporary + * files within the repository's <code>objects</code> directory. To use the + * data contained within them call {@link #renameAndOpenPack()} once the + * indexing is complete. + * + * @param db + * the repository that will receive the new pack. + * @param is + * stream to read the pack data from. If the stream is buffered + * use {@link #BUFFER_SIZE} as the buffer size for the stream. + * @return a new index pack instance. + * @throws IOException + * a temporary file could not be created. + */ + public static IndexPack create(final Repository db, final InputStream is) + throws IOException { + final String suffix = ".pack"; + final File objdir = db.getObjectsDirectory(); + final File tmp = File.createTempFile("incoming_", suffix, objdir); + final String n = tmp.getName(); + final File base; + + base = new File(objdir, n.substring(0, n.length() - suffix.length())); + final IndexPack ip = new IndexPack(db, is, base); + ip.setIndexVersion(db.getConfig().getCore().getPackIndexVersion()); + return ip; + } + + private final Repository repo; + + private Inflater inflater; + + private final MessageDigest objectDigest; + + private final MutableObjectId tempObjectId; + + private InputStream in; + + private byte[] buf; + + private long bBase; + + private int bOffset; + + private int bAvail; + + private ObjectChecker objCheck; + + private boolean fixThin; + + private boolean keepEmpty; + + private int outputVersion; + + private final File dstPack; + + private final File dstIdx; + + private long objectCount; + + private PackedObjectInfo[] entries; + + private int deltaCount; + + private int entryCount; + + private final CRC32 crc = new CRC32(); + + private ObjectIdSubclassMap<DeltaChain> baseById; + + private LongMap<UnresolvedDelta> baseByPos; + + private byte[] objectData; + + private MessageDigest packDigest; + + private RandomAccessFile packOut; + + private byte[] packcsum; + + /** If {@link #fixThin} this is the last byte of the original checksum. */ + private long originalEOF; + + private WindowCursor readCurs; + + /** + * Create a new pack indexer utility. + * + * @param db + * @param src + * stream to read the pack data from. If the stream is buffered + * use {@link #BUFFER_SIZE} as the buffer size for the stream. + * @param dstBase + * @throws IOException + * the output packfile could not be created. + */ + public IndexPack(final Repository db, final InputStream src, + final File dstBase) throws IOException { + repo = db; + in = src; + inflater = InflaterCache.get(); + readCurs = new WindowCursor(); + buf = new byte[BUFFER_SIZE]; + objectData = new byte[BUFFER_SIZE]; + objectDigest = Constants.newMessageDigest(); + tempObjectId = new MutableObjectId(); + packDigest = Constants.newMessageDigest(); + + if (dstBase != null) { + final File dir = dstBase.getParentFile(); + final String nam = dstBase.getName(); + dstPack = new File(dir, nam + ".pack"); + dstIdx = new File(dir, nam + ".idx"); + packOut = new RandomAccessFile(dstPack, "rw"); + packOut.setLength(0); + } else { + dstPack = null; + dstIdx = null; + } + } + + /** + * Set the pack index file format version this instance will create. + * + * @param version + * the version to write. The special version 0 designates the + * oldest (most compatible) format available for the objects. + * @see PackIndexWriter + */ + public void setIndexVersion(final int version) { + outputVersion = version; + } + + /** + * Configure this index pack instance to make a thin pack complete. + * <p> + * Thin packs are sometimes used during network transfers to allow a delta + * to be sent without a base object. Such packs are not permitted on disk. + * They can be fixed by copying the base object onto the end of the pack. + * + * @param fix + * true to enable fixing a thin pack. + */ + public void setFixThin(final boolean fix) { + fixThin = fix; + } + + /** + * Configure this index pack instance to keep an empty pack. + * <p> + * By default an empty pack (a pack with no objects) is not kept, as doing + * so is completely pointless. With no objects in the pack there is no data + * stored by it, so the pack is unnecessary. + * + * @param empty true to enable keeping an empty pack. + */ + public void setKeepEmpty(final boolean empty) { + keepEmpty = empty; + } + + /** + * Configure the checker used to validate received objects. + * <p> + * Usually object checking isn't necessary, as Git implementations only + * create valid objects in pack files. However, additional checking may be + * useful if processing data from an untrusted source. + * + * @param oc + * the checker instance; null to disable object checking. + */ + public void setObjectChecker(final ObjectChecker oc) { + objCheck = oc; + } + + /** + * Configure the checker used to validate received objects. + * <p> + * Usually object checking isn't necessary, as Git implementations only + * create valid objects in pack files. However, additional checking may be + * useful if processing data from an untrusted source. + * <p> + * This is shorthand for: + * + * <pre> + * setObjectChecker(on ? new ObjectChecker() : null); + * </pre> + * + * @param on + * true to enable the default checker; false to disable it. + */ + public void setObjectChecking(final boolean on) { + setObjectChecker(on ? new ObjectChecker() : null); + } + + /** + * Consume data from the input stream until the packfile is indexed. + * + * @param progress + * progress feedback + * + * @throws IOException + */ + public void index(final ProgressMonitor progress) throws IOException { + progress.start(2 /* tasks */); + try { + try { + readPackHeader(); + + entries = new PackedObjectInfo[(int) objectCount]; + baseById = new ObjectIdSubclassMap<DeltaChain>(); + baseByPos = new LongMap<UnresolvedDelta>(); + + progress.beginTask(PROGRESS_DOWNLOAD, (int) objectCount); + for (int done = 0; done < objectCount; done++) { + indexOneObject(); + progress.update(1); + if (progress.isCancelled()) + throw new IOException("Download cancelled"); + } + readPackFooter(); + endInput(); + progress.endTask(); + if (deltaCount > 0) { + if (packOut == null) + throw new IOException("need packOut"); + resolveDeltas(progress); + if (entryCount < objectCount) { + if (!fixThin) { + throw new IOException("pack has " + + (objectCount - entryCount) + + " unresolved deltas"); + } + fixThinPack(progress); + } + } + if (packOut != null && (keepEmpty || entryCount > 0)) + packOut.getChannel().force(true); + + packDigest = null; + baseById = null; + baseByPos = null; + + if (dstIdx != null && (keepEmpty || entryCount > 0)) + writeIdx(); + + } finally { + try { + InflaterCache.release(inflater); + } finally { + inflater = null; + } + readCurs = WindowCursor.release(readCurs); + + progress.endTask(); + if (packOut != null) + packOut.close(); + } + + if (keepEmpty || entryCount > 0) { + if (dstPack != null) + dstPack.setReadOnly(); + if (dstIdx != null) + dstIdx.setReadOnly(); + } + } catch (IOException err) { + if (dstPack != null) + dstPack.delete(); + if (dstIdx != null) + dstIdx.delete(); + throw err; + } + } + + private void resolveDeltas(final ProgressMonitor progress) + throws IOException { + progress.beginTask(PROGRESS_RESOLVE_DELTA, deltaCount); + final int last = entryCount; + for (int i = 0; i < last; i++) { + final int before = entryCount; + resolveDeltas(entries[i]); + progress.update(entryCount - before); + if (progress.isCancelled()) + throw new IOException("Download cancelled during indexing"); + } + progress.endTask(); + } + + private void resolveDeltas(final PackedObjectInfo oe) throws IOException { + final int oldCRC = oe.getCRC(); + if (baseById.get(oe) != null || baseByPos.containsKey(oe.getOffset())) + resolveDeltas(oe.getOffset(), oldCRC, Constants.OBJ_BAD, null, oe); + } + + private void resolveDeltas(final long pos, final int oldCRC, int type, + byte[] data, PackedObjectInfo oe) throws IOException { + crc.reset(); + position(pos); + int c = readFromFile(); + final int typeCode = (c >> 4) & 7; + long sz = c & 15; + int shift = 4; + while ((c & 0x80) != 0) { + c = readFromFile(); + sz += (c & 0x7f) << shift; + shift += 7; + } + + switch (typeCode) { + case Constants.OBJ_COMMIT: + case Constants.OBJ_TREE: + case Constants.OBJ_BLOB: + case Constants.OBJ_TAG: + type = typeCode; + data = inflateFromFile((int) sz); + break; + case Constants.OBJ_OFS_DELTA: { + c = readFromFile() & 0xff; + while ((c & 128) != 0) + c = readFromFile() & 0xff; + data = BinaryDelta.apply(data, inflateFromFile((int) sz)); + break; + } + case Constants.OBJ_REF_DELTA: { + crc.update(buf, fillFromFile(20), 20); + use(20); + data = BinaryDelta.apply(data, inflateFromFile((int) sz)); + break; + } + default: + throw new IOException("Unknown object type " + typeCode + "."); + } + + final int crc32 = (int) crc.getValue(); + if (oldCRC != crc32) + throw new IOException("Corruption detected re-reading at " + pos); + if (oe == null) { + objectDigest.update(Constants.encodedTypeString(type)); + objectDigest.update((byte) ' '); + objectDigest.update(Constants.encodeASCII(data.length)); + objectDigest.update((byte) 0); + objectDigest.update(data); + tempObjectId.fromRaw(objectDigest.digest(), 0); + + verifySafeObject(tempObjectId, type, data); + oe = new PackedObjectInfo(pos, crc32, tempObjectId); + entries[entryCount++] = oe; + } + + resolveChildDeltas(pos, type, data, oe); + } + + private UnresolvedDelta removeBaseById(final AnyObjectId id){ + final DeltaChain d = baseById.get(id); + return d != null ? d.remove() : null; + } + + private static UnresolvedDelta reverse(UnresolvedDelta c) { + UnresolvedDelta tail = null; + while (c != null) { + final UnresolvedDelta n = c.next; + c.next = tail; + tail = c; + c = n; + } + return tail; + } + + private void resolveChildDeltas(final long pos, int type, byte[] data, + PackedObjectInfo oe) throws IOException { + UnresolvedDelta a = reverse(removeBaseById(oe)); + UnresolvedDelta b = reverse(baseByPos.remove(pos)); + while (a != null && b != null) { + if (a.position < b.position) { + resolveDeltas(a.position, a.crc, type, data, null); + a = a.next; + } else { + resolveDeltas(b.position, b.crc, type, data, null); + b = b.next; + } + } + resolveChildDeltaChain(type, data, a); + resolveChildDeltaChain(type, data, b); + } + + private void resolveChildDeltaChain(final int type, final byte[] data, + UnresolvedDelta a) throws IOException { + while (a != null) { + resolveDeltas(a.position, a.crc, type, data, null); + a = a.next; + } + } + + private void fixThinPack(final ProgressMonitor progress) throws IOException { + growEntries(); + + packDigest.reset(); + originalEOF = packOut.length() - 20; + final Deflater def = new Deflater(Deflater.DEFAULT_COMPRESSION, false); + final List<DeltaChain> missing = new ArrayList<DeltaChain>(64); + long end = originalEOF; + for (final DeltaChain baseId : baseById) { + if (baseId.head == null) + continue; + final ObjectLoader ldr = repo.openObject(readCurs, baseId); + if (ldr == null) { + missing.add(baseId); + continue; + } + final byte[] data = ldr.getCachedBytes(); + final int typeCode = ldr.getType(); + final PackedObjectInfo oe; + + crc.reset(); + packOut.seek(end); + writeWhole(def, typeCode, data); + oe = new PackedObjectInfo(end, (int) crc.getValue(), baseId); + entries[entryCount++] = oe; + end = packOut.getFilePointer(); + + resolveChildDeltas(oe.getOffset(), typeCode, data, oe); + if (progress.isCancelled()) + throw new IOException("Download cancelled during indexing"); + } + def.end(); + + for (final DeltaChain base : missing) { + if (base.head != null) + throw new MissingObjectException(base, "delta base"); + } + + fixHeaderFooter(packcsum, packDigest.digest()); + } + + private void writeWhole(final Deflater def, final int typeCode, + final byte[] data) throws IOException { + int sz = data.length; + int hdrlen = 0; + buf[hdrlen++] = (byte) ((typeCode << 4) | sz & 15); + sz >>>= 4; + while (sz > 0) { + buf[hdrlen - 1] |= 0x80; + buf[hdrlen++] = (byte) (sz & 0x7f); + sz >>>= 7; + } + packDigest.update(buf, 0, hdrlen); + crc.update(buf, 0, hdrlen); + packOut.write(buf, 0, hdrlen); + def.reset(); + def.setInput(data); + def.finish(); + while (!def.finished()) { + final int datlen = def.deflate(buf); + packDigest.update(buf, 0, datlen); + crc.update(buf, 0, datlen); + packOut.write(buf, 0, datlen); + } + } + + private void fixHeaderFooter(final byte[] origcsum, final byte[] tailcsum) + throws IOException { + final MessageDigest origDigest = Constants.newMessageDigest(); + final MessageDigest tailDigest = Constants.newMessageDigest(); + long origRemaining = originalEOF; + + packOut.seek(0); + bAvail = 0; + bOffset = 0; + fillFromFile(12); + + { + final int origCnt = (int) Math.min(bAvail, origRemaining); + origDigest.update(buf, 0, origCnt); + origRemaining -= origCnt; + if (origRemaining == 0) + tailDigest.update(buf, origCnt, bAvail - origCnt); + } + + NB.encodeInt32(buf, 8, entryCount); + packOut.seek(0); + packOut.write(buf, 0, 12); + packOut.seek(bAvail); + + packDigest.reset(); + packDigest.update(buf, 0, bAvail); + for (;;) { + final int n = packOut.read(buf); + if (n < 0) + break; + if (origRemaining != 0) { + final int origCnt = (int) Math.min(n, origRemaining); + origDigest.update(buf, 0, origCnt); + origRemaining -= origCnt; + if (origRemaining == 0) + tailDigest.update(buf, origCnt, n - origCnt); + } else + tailDigest.update(buf, 0, n); + + packDigest.update(buf, 0, n); + } + + if (!Arrays.equals(origDigest.digest(), origcsum) + || !Arrays.equals(tailDigest.digest(), tailcsum)) + throw new IOException("Pack corrupted while writing to filesystem"); + + packcsum = packDigest.digest(); + packOut.write(packcsum); + } + + private void growEntries() { + final PackedObjectInfo[] ne; + + ne = new PackedObjectInfo[(int) objectCount + baseById.size()]; + System.arraycopy(entries, 0, ne, 0, entryCount); + entries = ne; + } + + private void writeIdx() throws IOException { + Arrays.sort(entries, 0, entryCount); + List<PackedObjectInfo> list = Arrays.asList(entries); + if (entryCount < entries.length) + list = list.subList(0, entryCount); + + final FileOutputStream os = new FileOutputStream(dstIdx); + try { + final PackIndexWriter iw; + if (outputVersion <= 0) + iw = PackIndexWriter.createOldestPossible(os, list); + else + iw = PackIndexWriter.createVersion(os, outputVersion); + iw.write(list, packcsum); + os.getChannel().force(true); + } finally { + os.close(); + } + } + + private void readPackHeader() throws IOException { + final int hdrln = Constants.PACK_SIGNATURE.length + 4 + 4; + final int p = fillFromInput(hdrln); + for (int k = 0; k < Constants.PACK_SIGNATURE.length; k++) + if (buf[p + k] != Constants.PACK_SIGNATURE[k]) + throw new IOException("Not a PACK file."); + + final long vers = NB.decodeUInt32(buf, p + 4); + if (vers != 2 && vers != 3) + throw new IOException("Unsupported pack version " + vers + "."); + objectCount = NB.decodeUInt32(buf, p + 8); + use(hdrln); + } + + private void readPackFooter() throws IOException { + sync(); + final byte[] cmpcsum = packDigest.digest(); + final int c = fillFromInput(20); + packcsum = new byte[20]; + System.arraycopy(buf, c, packcsum, 0, 20); + use(20); + if (packOut != null) + packOut.write(packcsum); + + if (!Arrays.equals(cmpcsum, packcsum)) + throw new CorruptObjectException("Packfile checksum incorrect."); + } + + // Cleanup all resources associated with our input parsing. + private void endInput() { + in = null; + objectData = null; + } + + // Read one entire object or delta from the input. + private void indexOneObject() throws IOException { + final long pos = position(); + + crc.reset(); + int c = readFromInput(); + final int typeCode = (c >> 4) & 7; + long sz = c & 15; + int shift = 4; + while ((c & 0x80) != 0) { + c = readFromInput(); + sz += (c & 0x7f) << shift; + shift += 7; + } + + switch (typeCode) { + case Constants.OBJ_COMMIT: + case Constants.OBJ_TREE: + case Constants.OBJ_BLOB: + case Constants.OBJ_TAG: + whole(typeCode, pos, sz); + break; + case Constants.OBJ_OFS_DELTA: { + c = readFromInput(); + long ofs = c & 127; + while ((c & 128) != 0) { + ofs += 1; + c = readFromInput(); + ofs <<= 7; + ofs += (c & 127); + } + final long base = pos - ofs; + final UnresolvedDelta n; + skipInflateFromInput(sz); + n = new UnresolvedDelta(pos, (int) crc.getValue()); + n.next = baseByPos.put(base, n); + deltaCount++; + break; + } + case Constants.OBJ_REF_DELTA: { + c = fillFromInput(20); + crc.update(buf, c, 20); + final ObjectId base = ObjectId.fromRaw(buf, c); + use(20); + DeltaChain r = baseById.get(base); + if (r == null) { + r = new DeltaChain(base); + baseById.add(r); + } + skipInflateFromInput(sz); + r.add(new UnresolvedDelta(pos, (int) crc.getValue())); + deltaCount++; + break; + } + default: + throw new IOException("Unknown object type " + typeCode + "."); + } + } + + private void whole(final int type, final long pos, final long sz) + throws IOException { + final byte[] data = inflateFromInput(sz); + objectDigest.update(Constants.encodedTypeString(type)); + objectDigest.update((byte) ' '); + objectDigest.update(Constants.encodeASCII(sz)); + objectDigest.update((byte) 0); + objectDigest.update(data); + tempObjectId.fromRaw(objectDigest.digest(), 0); + + verifySafeObject(tempObjectId, type, data); + final int crc32 = (int) crc.getValue(); + entries[entryCount++] = new PackedObjectInfo(pos, crc32, tempObjectId); + } + + private void verifySafeObject(final AnyObjectId id, final int type, + final byte[] data) throws IOException { + if (objCheck != null) { + try { + objCheck.check(type, data); + } catch (CorruptObjectException e) { + throw new IOException("Invalid " + + Constants.typeString(type) + " " + id.name() + + ":" + e.getMessage()); + } + } + + final ObjectLoader ldr = repo.openObject(readCurs, id); + if (ldr != null) { + final byte[] existingData = ldr.getCachedBytes(); + if (ldr.getType() != type || !Arrays.equals(data, existingData)) { + throw new IOException("Collision on " + id.name()); + } + } + } + + // Current position of {@link #bOffset} within the entire file. + private long position() { + return bBase + bOffset; + } + + private void position(final long pos) throws IOException { + packOut.seek(pos); + bBase = pos; + bOffset = 0; + bAvail = 0; + } + + // Consume exactly one byte from the buffer and return it. + private int readFromInput() throws IOException { + if (bAvail == 0) + fillFromInput(1); + bAvail--; + final int b = buf[bOffset++] & 0xff; + crc.update(b); + return b; + } + + // Consume exactly one byte from the buffer and return it. + private int readFromFile() throws IOException { + if (bAvail == 0) + fillFromFile(1); + bAvail--; + final int b = buf[bOffset++] & 0xff; + crc.update(b); + return b; + } + + // Consume cnt bytes from the buffer. + private void use(final int cnt) { + bOffset += cnt; + bAvail -= cnt; + } + + // Ensure at least need bytes are available in in {@link #buf}. + private int fillFromInput(final int need) throws IOException { + while (bAvail < need) { + int next = bOffset + bAvail; + int free = buf.length - next; + if (free + bAvail < need) { + sync(); + next = bAvail; + free = buf.length - next; + } + next = in.read(buf, next, free); + if (next <= 0) + throw new EOFException("Packfile is truncated."); + bAvail += next; + } + return bOffset; + } + + // Ensure at least need bytes are available in in {@link #buf}. + private int fillFromFile(final int need) throws IOException { + if (bAvail < need) { + int next = bOffset + bAvail; + int free = buf.length - next; + if (free + bAvail < need) { + if (bAvail > 0) + System.arraycopy(buf, bOffset, buf, 0, bAvail); + bOffset = 0; + next = bAvail; + free = buf.length - next; + } + next = packOut.read(buf, next, free); + if (next <= 0) + throw new EOFException("Packfile is truncated."); + bAvail += next; + } + return bOffset; + } + + // Store consumed bytes in {@link #buf} up to {@link #bOffset}. + private void sync() throws IOException { + packDigest.update(buf, 0, bOffset); + if (packOut != null) + packOut.write(buf, 0, bOffset); + if (bAvail > 0) + System.arraycopy(buf, bOffset, buf, 0, bAvail); + bBase += bOffset; + bOffset = 0; + } + + private void skipInflateFromInput(long sz) throws IOException { + final Inflater inf = inflater; + try { + final byte[] dst = objectData; + int n = 0; + int p = -1; + while (!inf.finished()) { + if (inf.needsInput()) { + if (p >= 0) { + crc.update(buf, p, bAvail); + use(bAvail); + } + p = fillFromInput(1); + inf.setInput(buf, p, bAvail); + } + + int free = dst.length - n; + if (free < 8) { + sz -= n; + n = 0; + free = dst.length; + } + n += inf.inflate(dst, n, free); + } + if (n != sz) + throw new DataFormatException("wrong decompressed length"); + n = bAvail - inf.getRemaining(); + if (n > 0) { + crc.update(buf, p, n); + use(n); + } + } catch (DataFormatException dfe) { + throw corrupt(dfe); + } finally { + inf.reset(); + } + } + + private byte[] inflateFromInput(final long sz) throws IOException { + final byte[] dst = new byte[(int) sz]; + final Inflater inf = inflater; + try { + int n = 0; + int p = -1; + while (!inf.finished()) { + if (inf.needsInput()) { + if (p >= 0) { + crc.update(buf, p, bAvail); + use(bAvail); + } + p = fillFromInput(1); + inf.setInput(buf, p, bAvail); + } + + n += inf.inflate(dst, n, dst.length - n); + } + if (n != sz) + throw new DataFormatException("wrong decompressed length"); + n = bAvail - inf.getRemaining(); + if (n > 0) { + crc.update(buf, p, n); + use(n); + } + return dst; + } catch (DataFormatException dfe) { + throw corrupt(dfe); + } finally { + inf.reset(); + } + } + + private byte[] inflateFromFile(final int sz) throws IOException { + final Inflater inf = inflater; + try { + final byte[] dst = new byte[sz]; + int n = 0; + int p = -1; + while (!inf.finished()) { + if (inf.needsInput()) { + if (p >= 0) { + crc.update(buf, p, bAvail); + use(bAvail); + } + p = fillFromFile(1); + inf.setInput(buf, p, bAvail); + } + n += inf.inflate(dst, n, sz - n); + } + n = bAvail - inf.getRemaining(); + if (n > 0) { + crc.update(buf, p, n); + use(n); + } + return dst; + } catch (DataFormatException dfe) { + throw corrupt(dfe); + } finally { + inf.reset(); + } + } + + private static CorruptObjectException corrupt(final DataFormatException dfe) { + return new CorruptObjectException("Packfile corruption detected: " + + dfe.getMessage()); + } + + private static class DeltaChain extends ObjectId { + UnresolvedDelta head; + + DeltaChain(final AnyObjectId id) { + super(id); + } + + UnresolvedDelta remove() { + final UnresolvedDelta r = head; + if (r != null) + head = null; + return r; + } + + void add(final UnresolvedDelta d) { + d.next = head; + head = d; + } + } + + private static class UnresolvedDelta { + final long position; + + final int crc; + + UnresolvedDelta next; + + UnresolvedDelta(final long headerOffset, final int crc32) { + position = headerOffset; + crc = crc32; + } + } + + /** + * Rename the pack to it's final name and location and open it. + * <p> + * If the call completes successfully the repository this IndexPack instance + * was created with will have the objects in the pack available for reading + * and use, without needing to scan for packs. + * + * @throws IOException + * The pack could not be inserted into the repository's objects + * directory. The pack no longer exists on disk, as it was + * removed prior to throwing the exception to the caller. + */ + public void renameAndOpenPack() throws IOException { + renameAndOpenPack(null); + } + + /** + * Rename the pack to it's final name and location and open it. + * <p> + * If the call completes successfully the repository this IndexPack instance + * was created with will have the objects in the pack available for reading + * and use, without needing to scan for packs. + * + * @param lockMessage + * message to place in the pack-*.keep file. If null, no lock + * will be created, and this method returns null. + * @return the pack lock object, if lockMessage is not null. + * @throws IOException + * The pack could not be inserted into the repository's objects + * directory. The pack no longer exists on disk, as it was + * removed prior to throwing the exception to the caller. + */ + public PackLock renameAndOpenPack(final String lockMessage) + throws IOException { + if (!keepEmpty && entryCount == 0) { + cleanupTemporaryFiles(); + return null; + } + + final MessageDigest d = Constants.newMessageDigest(); + final byte[] oeBytes = new byte[Constants.OBJECT_ID_LENGTH]; + for (int i = 0; i < entryCount; i++) { + final PackedObjectInfo oe = entries[i]; + oe.copyRawTo(oeBytes, 0); + d.update(oeBytes); + } + + final String name = ObjectId.fromRaw(d.digest()).name(); + final File packDir = new File(repo.getObjectsDirectory(), "pack"); + final File finalPack = new File(packDir, "pack-" + name + ".pack"); + final File finalIdx = new File(packDir, "pack-" + name + ".idx"); + final PackLock keep = new PackLock(finalPack); + + if (!packDir.exists() && !packDir.mkdir() && !packDir.exists()) { + // The objects/pack directory isn't present, and we are unable + // to create it. There is no way to move this pack in. + // + cleanupTemporaryFiles(); + throw new IOException("Cannot create " + packDir.getAbsolutePath()); + } + + if (finalPack.exists()) { + // If the pack is already present we should never replace it. + // + cleanupTemporaryFiles(); + return null; + } + + if (lockMessage != null) { + // If we have a reason to create a keep file for this pack, do + // so, or fail fast and don't put the pack in place. + // + try { + if (!keep.lock(lockMessage)) + throw new IOException("Cannot lock pack in " + finalPack); + } catch (IOException e) { + cleanupTemporaryFiles(); + throw e; + } + } + + if (!dstPack.renameTo(finalPack)) { + cleanupTemporaryFiles(); + keep.unlock(); + throw new IOException("Cannot move pack to " + finalPack); + } + + if (!dstIdx.renameTo(finalIdx)) { + cleanupTemporaryFiles(); + keep.unlock(); + if (!finalPack.delete()) + finalPack.deleteOnExit(); + throw new IOException("Cannot move index to " + finalIdx); + } + + try { + repo.openPack(finalPack, finalIdx); + } catch (IOException err) { + keep.unlock(); + finalPack.delete(); + finalIdx.delete(); + throw err; + } + + return lockMessage != null ? keep : null; + } + + private void cleanupTemporaryFiles() { + if (!dstIdx.delete()) + dstIdx.deleteOnExit(); + if (!dstPack.delete()) + dstPack.deleteOnExit(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/LongMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/LongMap.java new file mode 100644 index 0000000000..6381c24dcc --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/LongMap.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +/** + * Simple Map<long,Object> helper for {@link IndexPack}. + * + * @param <V> + * type of the value instance. + */ +final class LongMap<V> { + private static final float LOAD_FACTOR = 0.75f; + + private Node<V>[] table; + + /** Number of entries currently in the map. */ + private int size; + + /** Next {@link #size} to trigger a {@link #grow()}. */ + private int growAt; + + LongMap() { + table = createArray(64); + growAt = (int) (table.length * LOAD_FACTOR); + } + + boolean containsKey(final long key) { + return get(key) != null; + } + + V get(final long key) { + for (Node<V> n = table[index(key)]; n != null; n = n.next) { + if (n.key == key) + return n.value; + } + return null; + } + + V remove(final long key) { + Node<V> n = table[index(key)]; + Node<V> prior = null; + while (n != null) { + if (n.key == key) { + if (prior == null) + table[index(key)] = n.next; + else + prior.next = n.next; + size--; + return n.value; + } + prior = n; + n = n.next; + } + return null; + } + + V put(final long key, final V value) { + for (Node<V> n = table[index(key)]; n != null; n = n.next) { + if (n.key == key) { + final V o = n.value; + n.value = value; + return o; + } + } + + if (++size == growAt) + grow(); + insert(new Node<V>(key, value)); + return null; + } + + private void insert(final Node<V> n) { + final int idx = index(n.key); + n.next = table[idx]; + table[idx] = n; + } + + private void grow() { + final Node<V>[] oldTable = table; + final int oldSize = table.length; + + table = createArray(oldSize << 1); + growAt = (int) (table.length * LOAD_FACTOR); + for (int i = 0; i < oldSize; i++) { + Node<V> e = oldTable[i]; + while (e != null) { + final Node<V> n = e.next; + insert(e); + e = n; + } + } + } + + private final int index(final long key) { + int h = ((int) key) >>> 1; + h ^= (h >>> 20) ^ (h >>> 12); + return h & (table.length - 1); + } + + @SuppressWarnings("unchecked") + private static final <V> Node<V>[] createArray(final int sz) { + return new Node[sz]; + } + + private static class Node<V> { + final long key; + + V value; + + Node<V> next; + + Node(final long k, final V v) { + key = k; + value = v; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java new file mode 100644 index 0000000000..e7a307f809 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OpenSshConfig.java @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.errors.InvalidPatternException; +import org.eclipse.jgit.fnmatch.FileNameMatcher; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.StringUtils; + +/** + * Simple configuration parser for the OpenSSH ~/.ssh/config file. + * <p> + * Since JSch does not (currently) have the ability to parse an OpenSSH + * configuration file this is a simple parser to read that file and make the + * critical options available to {@link SshSessionFactory}. + */ +public class OpenSshConfig { + /** IANA assigned port number for SSH. */ + static final int SSH_PORT = 22; + + /** + * Obtain the user's configuration data. + * <p> + * The configuration file is always returned to the caller, even if no file + * exists in the user's home directory at the time the call was made. Lookup + * requests are cached and are automatically updated if the user modifies + * the configuration file since the last time it was cached. + * + * @return a caching reader of the user's configuration file. + */ + public static OpenSshConfig get() { + File home = FS.userHome(); + if (home == null) + home = new File(".").getAbsoluteFile(); + + final File config = new File(new File(home, ".ssh"), "config"); + final OpenSshConfig osc = new OpenSshConfig(home, config); + osc.refresh(); + return osc; + } + + /** The user's home directory, as key files may be relative to here. */ + private final File home; + + /** The .ssh/config file we read and monitor for updates. */ + private final File configFile; + + /** Modification time of {@link #configFile} when {@link #hosts} loaded. */ + private long lastModified; + + /** Cached entries read out of the configuration file. */ + private Map<String, Host> hosts; + + OpenSshConfig(final File h, final File cfg) { + home = h; + configFile = cfg; + hosts = Collections.emptyMap(); + } + + /** + * Locate the configuration for a specific host request. + * + * @param hostName + * the name the user has supplied to the SSH tool. This may be a + * real host name, or it may just be a "Host" block in the + * configuration file. + * @return r configuration for the requested name. Never null. + */ + public Host lookup(final String hostName) { + final Map<String, Host> cache = refresh(); + Host h = cache.get(hostName); + if (h == null) + h = new Host(); + if (h.patternsApplied) + return h; + + for (final Map.Entry<String, Host> e : cache.entrySet()) { + if (!isHostPattern(e.getKey())) + continue; + if (!isHostMatch(e.getKey(), hostName)) + continue; + h.copyFrom(e.getValue()); + } + + if (h.hostName == null) + h.hostName = hostName; + if (h.user == null) + h.user = OpenSshConfig.userName(); + if (h.port == 0) + h.port = OpenSshConfig.SSH_PORT; + h.patternsApplied = true; + return h; + } + + private synchronized Map<String, Host> refresh() { + final long mtime = configFile.lastModified(); + if (mtime != lastModified) { + try { + final FileInputStream in = new FileInputStream(configFile); + try { + hosts = parse(in); + } finally { + in.close(); + } + } catch (FileNotFoundException none) { + hosts = Collections.emptyMap(); + } catch (IOException err) { + hosts = Collections.emptyMap(); + } + lastModified = mtime; + } + return hosts; + } + + private Map<String, Host> parse(final InputStream in) throws IOException { + final Map<String, Host> m = new LinkedHashMap<String, Host>(); + final BufferedReader br = new BufferedReader(new InputStreamReader(in)); + final List<Host> current = new ArrayList<Host>(4); + String line; + + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.length() == 0 || line.startsWith("#")) + continue; + + final String[] parts = line.split("[ \t]*[= \t]", 2); + final String keyword = parts[0].trim(); + final String argValue = parts[1].trim(); + + if (StringUtils.equalsIgnoreCase("Host", keyword)) { + current.clear(); + for (final String pattern : argValue.split("[ \t]")) { + final String name = dequote(pattern); + Host c = m.get(name); + if (c == null) { + c = new Host(); + m.put(name, c); + } + current.add(c); + } + continue; + } + + if (current.isEmpty()) { + // We received an option outside of a Host block. We + // don't know who this should match against, so skip. + // + continue; + } + + if (StringUtils.equalsIgnoreCase("HostName", keyword)) { + for (final Host c : current) + if (c.hostName == null) + c.hostName = dequote(argValue); + } else if (StringUtils.equalsIgnoreCase("User", keyword)) { + for (final Host c : current) + if (c.user == null) + c.user = dequote(argValue); + } else if (StringUtils.equalsIgnoreCase("Port", keyword)) { + try { + final int port = Integer.parseInt(dequote(argValue)); + for (final Host c : current) + if (c.port == 0) + c.port = port; + } catch (NumberFormatException nfe) { + // Bad port number. Don't set it. + } + } else if (StringUtils.equalsIgnoreCase("IdentityFile", keyword)) { + for (final Host c : current) + if (c.identityFile == null) + c.identityFile = toFile(dequote(argValue)); + } else if (StringUtils.equalsIgnoreCase("PreferredAuthentications", keyword)) { + for (final Host c : current) + if (c.preferredAuthentications == null) + c.preferredAuthentications = nows(dequote(argValue)); + } else if (StringUtils.equalsIgnoreCase("BatchMode", keyword)) { + for (final Host c : current) + if (c.batchMode == null) + c.batchMode = yesno(dequote(argValue)); + } else if (StringUtils.equalsIgnoreCase("StrictHostKeyChecking", keyword)) { + String value = dequote(argValue); + for (final Host c : current) + if (c.strictHostKeyChecking == null) + c.strictHostKeyChecking = value; + } + } + + return m; + } + + private static boolean isHostPattern(final String s) { + return s.indexOf('*') >= 0 || s.indexOf('?') >= 0; + } + + private static boolean isHostMatch(final String pattern, final String name) { + final FileNameMatcher fn; + try { + fn = new FileNameMatcher(pattern, null); + } catch (InvalidPatternException e) { + return false; + } + fn.append(name); + return fn.isMatch(); + } + + private static String dequote(final String value) { + if (value.startsWith("\"") && value.endsWith("\"")) + return value.substring(1, value.length() - 1); + return value; + } + + private static String nows(final String value) { + final StringBuilder b = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + if (!Character.isSpaceChar(value.charAt(i))) + b.append(value.charAt(i)); + } + return b.toString(); + } + + private static Boolean yesno(final String value) { + if (StringUtils.equalsIgnoreCase("yes", value)) + return Boolean.TRUE; + return Boolean.FALSE; + } + + private File toFile(final String path) { + if (path.startsWith("~/")) + return new File(home, path.substring(2)); + File ret = new File(path); + if (ret.isAbsolute()) + return ret; + return new File(home, path); + } + + static String userName() { + return AccessController.doPrivileged(new PrivilegedAction<String>() { + public String run() { + return System.getProperty("user.name"); + } + }); + } + + /** + * Configuration of one "Host" block in the configuration file. + * <p> + * If returned from {@link OpenSshConfig#lookup(String)} some or all of the + * properties may not be populated. The properties which are not populated + * should be defaulted by the caller. + * <p> + * When returned from {@link OpenSshConfig#lookup(String)} any wildcard + * entries which appear later in the configuration file will have been + * already merged into this block. + */ + public static class Host { + boolean patternsApplied; + + String hostName; + + int port; + + File identityFile; + + String user; + + String preferredAuthentications; + + Boolean batchMode; + + String strictHostKeyChecking; + + void copyFrom(final Host src) { + if (hostName == null) + hostName = src.hostName; + if (port == 0) + port = src.port; + if (identityFile == null) + identityFile = src.identityFile; + if (user == null) + user = src.user; + if (preferredAuthentications == null) + preferredAuthentications = src.preferredAuthentications; + if (batchMode == null) + batchMode = src.batchMode; + if (strictHostKeyChecking == null) + strictHostKeyChecking = src.strictHostKeyChecking; + } + + /** + * @return the value StrictHostKeyChecking property, the valid values + * are "yes" (unknown hosts are not accepted), "no" (unknown + * hosts are always accepted), and "ask" (user should be asked + * before accepting the host) + */ + public String getStrictHostKeyChecking() { + return strictHostKeyChecking; + } + + /** + * @return the real IP address or host name to connect to; never null. + */ + public String getHostName() { + return hostName; + } + + /** + * @return the real port number to connect to; never 0. + */ + public int getPort() { + return port; + } + + /** + * @return path of the private key file to use for authentication; null + * if the caller should use default authentication strategies. + */ + public File getIdentityFile() { + return identityFile; + } + + /** + * @return the real user name to connect as; never null. + */ + public String getUser() { + return user; + } + + /** + * @return the preferred authentication methods, separated by commas if + * more than one authentication method is preferred. + */ + public String getPreferredAuthentications() { + return preferredAuthentications; + } + + /** + * @return true if batch (non-interactive) mode is preferred for this + * host connection. + */ + public boolean isBatchMode() { + return batchMode != null && batchMode.booleanValue(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/OperationResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OperationResult.java new file mode 100644 index 0000000000..c7371d60f9 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OperationResult.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2007-2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.eclipse.jgit.lib.Ref; + +/** + * Class holding result of operation on remote repository. This includes refs + * advertised by remote repo and local tracking refs updates. + */ +public abstract class OperationResult { + + Map<String, Ref> advertisedRefs = Collections.emptyMap(); + + URIish uri; + + final SortedMap<String, TrackingRefUpdate> updates = new TreeMap<String, TrackingRefUpdate>(); + + /** + * Get the URI this result came from. + * <p> + * Each transport instance connects to at most one URI at any point in time. + * + * @return the URI describing the location of the remote repository. + */ + public URIish getURI() { + return uri; + } + + /** + * Get the complete list of refs advertised by the remote. + * <p> + * The returned refs may appear in any order. If the caller needs these to + * be sorted, they should be copied into a new array or List and then sorted + * by the caller as necessary. + * + * @return available/advertised refs. Never null. Not modifiable. The + * collection can be empty if the remote side has no refs (it is an + * empty/newly created repository). + */ + public Collection<Ref> getAdvertisedRefs() { + return Collections.unmodifiableCollection(advertisedRefs.values()); + } + + /** + * Get a single advertised ref by name. + * <p> + * The name supplied should be valid ref name. To get a peeled value for a + * ref (aka <code>refs/tags/v1.0^{}</code>) use the base name (without + * the <code>^{}</code> suffix) and look at the peeled object id. + * + * @param name + * name of the ref to obtain. + * @return the requested ref; null if the remote did not advertise this ref. + */ + public final Ref getAdvertisedRef(final String name) { + return advertisedRefs.get(name); + } + + /** + * Get the status of all local tracking refs that were updated. + * + * @return unmodifiable collection of local updates. Never null. Empty if + * there were no local tracking refs updated. + */ + public Collection<TrackingRefUpdate> getTrackingRefUpdates() { + return Collections.unmodifiableCollection(updates.values()); + } + + /** + * Get the status for a specific local tracking ref update. + * + * @param localName + * name of the local ref (e.g. "refs/remotes/origin/master"). + * @return status of the local ref; null if this local ref was not touched + * during this operation. + */ + public TrackingRefUpdate getTrackingRefUpdate(final String localName) { + return updates.get(localName); + } + + void setAdvertisedRefs(final URIish u, final Map<String, Ref> ar) { + uri = u; + advertisedRefs = ar; + } + + void add(final TrackingRefUpdate u) { + updates.put(u.getLocalName(), u); + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackTransport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackTransport.java new file mode 100644 index 0000000000..736d329653 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackTransport.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2009, JetBrains s.r.o. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +/** + * Marker interface an object transport using Git pack transfers. + * <p> + * Implementations of PackTransport setup connections and move objects back and + * forth by creating pack files on the source side and indexing them on the + * receiving side. + * + * @see BasePackFetchConnection + * @see BasePackPushConnection + */ +public interface PackTransport { + // no methods in marker interface +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackedObjectInfo.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackedObjectInfo.java new file mode 100644 index 0000000000..5071cc7995 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackedObjectInfo.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2008-2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; + +/** + * Description of an object stored in a pack file, including offset. + * <p> + * When objects are stored in packs Git needs the ObjectId and the offset + * (starting position of the object data) to perform random-access reads of + * objects from the pack. This extension of ObjectId includes the offset. + */ +public class PackedObjectInfo extends ObjectId { + private long offset; + + private int crc; + + PackedObjectInfo(final long headerOffset, final int packedCRC, + final AnyObjectId id) { + super(id); + offset = headerOffset; + crc = packedCRC; + } + + /** + * Create a new structure to remember information about an object. + * + * @param id + * the identity of the object the new instance tracks. + */ + public PackedObjectInfo(final AnyObjectId id) { + super(id); + } + + /** + * @return offset in pack when object has been already written, or 0 if it + * has not been written yet + */ + public long getOffset() { + return offset; + } + + /** + * Set the offset in pack when object has been written to. + * + * @param offset + * offset where written object starts + */ + public void setOffset(final long offset) { + this.offset = offset; + } + + /** + * @return the 32 bit CRC checksum for the packed data. + */ + public int getCRC() { + return crc; + } + + /** + * Record the 32 bit CRC checksum for the packed data. + * + * @param crc + * checksum of all packed data (including object type code, + * inflated length and delta base reference) as computed by + * {@link java.util.zip.CRC32}. + */ + public void setCRC(final int crc) { + this.crc = crc; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java new file mode 100644 index 0000000000..29fe831ae4 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; +import java.io.InputStream; + +import org.eclipse.jgit.errors.PackProtocolException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.MutableObjectId; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.RawParseUtils; + +class PacketLineIn { + static final String END = new String("") /* must not string pool */; + + static enum AckNackResult { + /** NAK */ + NAK, + /** ACK */ + ACK, + /** ACK + continue */ + ACK_CONTINUE + } + + private final InputStream in; + + private final byte[] lenbuffer; + + PacketLineIn(final InputStream i) { + in = i; + lenbuffer = new byte[4]; + } + + InputStream sideband(final ProgressMonitor pm) { + return new SideBandInputStream(this, in, pm); + } + + AckNackResult readACK(final MutableObjectId returnedId) throws IOException { + final String line = readString(); + if (line.length() == 0) + throw new PackProtocolException("Expected ACK/NAK, found EOF"); + if ("NAK".equals(line)) + return AckNackResult.NAK; + if (line.startsWith("ACK ")) { + returnedId.fromString(line.substring(4, 44)); + if (line.indexOf("continue", 44) != -1) + return AckNackResult.ACK_CONTINUE; + return AckNackResult.ACK; + } + throw new PackProtocolException("Expected ACK/NAK, got: " + line); + } + + String readString() throws IOException { + int len = readLength(); + if (len == 0) + return END; + + len -= 4; // length header (4 bytes) + if (len == 0) + return ""; + + final byte[] raw = new byte[len]; + NB.readFully(in, raw, 0, len); + if (raw[len - 1] == '\n') + len--; + return RawParseUtils.decode(Constants.CHARSET, raw, 0, len); + } + + String readStringRaw() throws IOException { + int len = readLength(); + if (len == 0) + return END; + + len -= 4; // length header (4 bytes) + + final byte[] raw = new byte[len]; + NB.readFully(in, raw, 0, len); + return RawParseUtils.decode(Constants.CHARSET, raw, 0, len); + } + + int readLength() throws IOException { + NB.readFully(in, lenbuffer, 0, 4); + try { + final int len = RawParseUtils.parseHexInt16(lenbuffer, 0); + if (len != 0 && len < 4) + throw new ArrayIndexOutOfBoundsException(); + return len; + } catch (ArrayIndexOutOfBoundsException err) { + throw new IOException("Invalid packet line header: " + + (char) lenbuffer[0] + (char) lenbuffer[1] + + (char) lenbuffer[2] + (char) lenbuffer[3]); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java new file mode 100644 index 0000000000..e7a7198d7c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008-2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; +import java.io.OutputStream; + +import org.eclipse.jgit.lib.Constants; + +class PacketLineOut { + private final OutputStream out; + + private final byte[] lenbuffer; + + PacketLineOut(final OutputStream i) { + out = i; + lenbuffer = new byte[5]; + } + + void writeString(final String s) throws IOException { + writePacket(Constants.encode(s)); + } + + void writePacket(final byte[] packet) throws IOException { + formatLength(packet.length + 4); + out.write(lenbuffer, 0, 4); + out.write(packet); + } + + void writeChannelPacket(final int channel, final byte[] buf, int off, + int len) throws IOException { + formatLength(len + 5); + lenbuffer[4] = (byte) channel; + out.write(lenbuffer, 0, 5); + out.write(buf, off, len); + } + + void end() throws IOException { + formatLength(0); + out.write(lenbuffer, 0, 4); + flush(); + } + + void flush() throws IOException { + out.flush(); + } + + private static final byte[] hexchar = { '0', '1', '2', '3', '4', '5', '6', + '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + private void formatLength(int w) { + int o = 3; + while (o >= 0 && w != 0) { + lenbuffer[o--] = hexchar[w & 0xf]; + w >>>= 4; + } + while (o >= 0) + lenbuffer[o--] = '0'; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostReceiveHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostReceiveHook.java new file mode 100644 index 0000000000..1e662751bc --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostReceiveHook.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.util.Collection; + +/** + * Hook invoked by {@link ReceivePack} after all updates are executed. + * <p> + * The hook is called after all commands have been processed. Only commands with + * a status of {@link ReceiveCommand.Result#OK} are passed into the hook. To get + * all commands within the hook, see {@link ReceivePack#getAllCommands()}. + * <p> + * Any post-receive hook implementation should not update the status of a + * command, as the command has already completed or failed, and the status has + * already been returned to the client. + * <p> + * Hooks should execute quickly, as they block the server and the client from + * completing the connection. + */ +public interface PostReceiveHook { + /** A simple no-op hook. */ + public static final PostReceiveHook NULL = new PostReceiveHook() { + public void onPostReceive(final ReceivePack rp, + final Collection<ReceiveCommand> commands) { + // Do nothing. + } + }; + + /** + * Invoked after all commands are executed and status has been returned. + * + * @param rp + * the process handling the current receive. Hooks may obtain + * details about the destination repository through this handle. + * @param commands + * unmodifiable set of successfully completed commands. May be + * the empty set. + */ + public void onPostReceive(ReceivePack rp, + Collection<ReceiveCommand> commands); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreReceiveHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreReceiveHook.java new file mode 100644 index 0000000000..9a743a515b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreReceiveHook.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.util.Collection; + +/** + * Hook invoked by {@link ReceivePack} before any updates are executed. + * <p> + * The hook is called with any commands that are deemed valid after parsing them + * from the client and applying the standard receive configuration options to + * them: + * <ul> + * <li><code>receive.denyDenyDeletes</code></li> + * <li><code>receive.denyNonFastForwards</code></li> + * </ul> + * This means the hook will not receive a non-fast-forward update command if + * denyNonFastForwards is set to true in the configuration file. To get all + * commands within the hook, see {@link ReceivePack#getAllCommands()}. + * <p> + * As the hook is invoked prior to the commands being executed, the hook may + * choose to block any command by setting its result status with + * {@link ReceiveCommand#setResult(ReceiveCommand.Result)}. + * <p> + * The hook may also choose to perform the command itself (or merely pretend + * that it has performed the command), by setting the result status to + * {@link ReceiveCommand.Result#OK}. + * <p> + * Hooks should run quickly, as they block the caller thread and the client + * process from completing. + * <p> + * Hooks may send optional messages back to the client via methods on + * {@link ReceivePack}. Implementors should be aware that not all network + * transports support this output, so some (or all) messages may simply be + * discarded. These messages should be advisory only. + */ +public interface PreReceiveHook { + /** A simple no-op hook. */ + public static final PreReceiveHook NULL = new PreReceiveHook() { + public void onPreReceive(final ReceivePack rp, + final Collection<ReceiveCommand> commands) { + // Do nothing. + } + }; + + /** + * Invoked just before commands are executed. + * <p> + * See the class description for how this method can impact execution. + * + * @param rp + * the process handling the current receive. Hooks may obtain + * details about the destination repository through this handle. + * @param commands + * unmodifiable set of valid commands still pending execution. + * May be the empty set. + */ + public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConnection.java new file mode 100644 index 0000000000..14e6a1e800 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConnection.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.util.Map; + +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.transport.RemoteRefUpdate.Status; + +/** + * Lists known refs from the remote and sends objects to the remote. + * <p> + * A push connection typically connects to the <code>git-receive-pack</code> + * service running where the remote repository is stored. This provides a + * one-way object transfer service to copy objects from the local repository + * into the remote repository, as well as a way to modify the refs stored by the + * remote repository. + * <p> + * Instances of a PushConnection must be created by a {@link Transport} that + * implements a specific object transfer protocol that both sides of the + * connection understand. + * <p> + * PushConnection instances are not thread safe and may be accessed by only one + * thread at a time. + * + * @see Transport + */ +public interface PushConnection extends Connection { + + /** + * Pushes to the remote repository basing on provided specification. This + * possibly result in update/creation/deletion of refs on remote repository + * and sending objects that remote repository need to have a consistent + * objects graph from new refs. + * <p> + * <p> + * Only one call per connection is allowed. Subsequent calls will result in + * {@link TransportException}. + * </p> + * <p> + * Implementation may use local repository to send a minimum set of objects + * needed by remote repository in efficient way. + * {@link Transport#isPushThin()} should be honored if applicable. + * refUpdates should be filled with information about status of each update. + * </p> + * + * @param monitor + * progress monitor to update the end-user about the amount of + * work completed, or to indicate cancellation. Implementors + * should poll the monitor at regular intervals to look for + * cancellation requests from the user. + * @param refUpdates + * map of remote refnames to remote refs update + * specifications/statuses. Can't be empty. This indicate what + * refs caller want to update on remote side. Only refs updates + * with {@link Status#NOT_ATTEMPTED} should passed. + * Implementation must ensure that and appropriate status with + * optional message should be set during call. No refUpdate with + * {@link Status#AWAITING_REPORT} or {@link Status#NOT_ATTEMPTED} + * can be leaved by implementation after return from this call. + * @throws TransportException + * objects could not be copied due to a network failure, + * critical protocol error, or error on remote side, or + * connection was already used for push - new connection must be + * created. Non-critical errors concerning only isolated refs + * should be placed in refUpdates. + */ + public void push(final ProgressMonitor monitor, + final Map<String, RemoteRefUpdate> refUpdates) + throws TransportException; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java new file mode 100644 index 0000000000..17e1dfc77b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.RemoteRefUpdate.Status; + +/** + * Class performing push operation on remote repository. + * + * @see Transport#push(ProgressMonitor, Collection) + */ +class PushProcess { + /** Task name for {@link ProgressMonitor} used during opening connection. */ + static final String PROGRESS_OPENING_CONNECTION = "Opening connection"; + + /** Transport used to perform this operation. */ + private final Transport transport; + + /** Push operation connection created to perform this operation */ + private PushConnection connection; + + /** Refs to update on remote side. */ + private final Map<String, RemoteRefUpdate> toPush; + + /** Revision walker for checking some updates properties. */ + private final RevWalk walker; + + /** + * Create process for specified transport and refs updates specification. + * + * @param transport + * transport between remote and local repository, used to create + * connection. + * @param toPush + * specification of refs updates (and local tracking branches). + * @throws TransportException + */ + PushProcess(final Transport transport, + final Collection<RemoteRefUpdate> toPush) throws TransportException { + this.walker = new RevWalk(transport.local); + this.transport = transport; + this.toPush = new HashMap<String, RemoteRefUpdate>(); + for (final RemoteRefUpdate rru : toPush) { + if (this.toPush.put(rru.getRemoteName(), rru) != null) + throw new TransportException( + "Duplicate remote ref update is illegal. Affected remote name: " + + rru.getRemoteName()); + } + } + + /** + * Perform push operation between local and remote repository - set remote + * refs appropriately, send needed objects and update local tracking refs. + * <p> + * When {@link Transport#isDryRun()} is true, result of this operation is + * just estimation of real operation result, no real action is performed. + * + * @param monitor + * progress monitor used for feedback about operation. + * @return result of push operation with complete status description. + * @throws NotSupportedException + * when push operation is not supported by provided transport. + * @throws TransportException + * when some error occurred during operation, like I/O, protocol + * error, or local database consistency error. + */ + PushResult execute(final ProgressMonitor monitor) + throws NotSupportedException, TransportException { + monitor.beginTask(PROGRESS_OPENING_CONNECTION, ProgressMonitor.UNKNOWN); + connection = transport.openPush(); + try { + monitor.endTask(); + + final Map<String, RemoteRefUpdate> preprocessed = prepareRemoteUpdates(); + if (transport.isDryRun()) + modifyUpdatesForDryRun(); + else if (!preprocessed.isEmpty()) + connection.push(monitor, preprocessed); + } finally { + connection.close(); + } + if (!transport.isDryRun()) + updateTrackingRefs(); + return prepareOperationResult(); + } + + private Map<String, RemoteRefUpdate> prepareRemoteUpdates() + throws TransportException { + final Map<String, RemoteRefUpdate> result = new HashMap<String, RemoteRefUpdate>(); + for (final RemoteRefUpdate rru : toPush.values()) { + final Ref advertisedRef = connection.getRef(rru.getRemoteName()); + final ObjectId advertisedOld = (advertisedRef == null ? ObjectId + .zeroId() : advertisedRef.getObjectId()); + + if (rru.getNewObjectId().equals(advertisedOld)) { + if (rru.isDelete()) { + // ref does exist neither locally nor remotely + rru.setStatus(Status.NON_EXISTING); + } else { + // same object - nothing to do + rru.setStatus(Status.UP_TO_DATE); + } + continue; + } + + // caller has explicitly specified expected old object id, while it + // has been changed in the mean time - reject + if (rru.isExpectingOldObjectId() + && !rru.getExpectedOldObjectId().equals(advertisedOld)) { + rru.setStatus(Status.REJECTED_REMOTE_CHANGED); + continue; + } + + // create ref (hasn't existed on remote side) and delete ref + // are always fast-forward commands, feasible at this level + if (advertisedOld.equals(ObjectId.zeroId()) || rru.isDelete()) { + rru.setFastForward(true); + result.put(rru.getRemoteName(), rru); + continue; + } + + // check for fast-forward: + // - both old and new ref must point to commits, AND + // - both of them must be known for us, exist in repository, AND + // - old commit must be ancestor of new commit + boolean fastForward = true; + try { + RevObject oldRev = walker.parseAny(advertisedOld); + final RevObject newRev = walker.parseAny(rru.getNewObjectId()); + if (!(oldRev instanceof RevCommit) + || !(newRev instanceof RevCommit) + || !walker.isMergedInto((RevCommit) oldRev, + (RevCommit) newRev)) + fastForward = false; + } catch (MissingObjectException x) { + fastForward = false; + } catch (Exception x) { + throw new TransportException(transport.getURI(), + "reading objects from local repository failed: " + + x.getMessage(), x); + } + rru.setFastForward(fastForward); + if (!fastForward && !rru.isForceUpdate()) + rru.setStatus(Status.REJECTED_NONFASTFORWARD); + else + result.put(rru.getRemoteName(), rru); + } + return result; + } + + private void modifyUpdatesForDryRun() { + for (final RemoteRefUpdate rru : toPush.values()) + if (rru.getStatus() == Status.NOT_ATTEMPTED) + rru.setStatus(Status.OK); + } + + private void updateTrackingRefs() { + for (final RemoteRefUpdate rru : toPush.values()) { + final Status status = rru.getStatus(); + if (rru.hasTrackingRefUpdate() + && (status == Status.UP_TO_DATE || status == Status.OK)) { + // update local tracking branch only when there is a chance that + // it has changed; this is possible for: + // -updated (OK) status, + // -up to date (UP_TO_DATE) status + try { + rru.updateTrackingRef(walker); + } catch (IOException e) { + // ignore as RefUpdate has stored I/O error status + } + } + } + } + + private PushResult prepareOperationResult() { + final PushResult result = new PushResult(); + result.setAdvertisedRefs(transport.getURI(), connection.getRefsMap()); + result.setRemoteUpdates(toPush); + + for (final RemoteRefUpdate rru : toPush.values()) { + final TrackingRefUpdate tru = rru.getTrackingRefUpdate(); + if (tru != null) + result.add(tru); + } + return result; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushResult.java new file mode 100644 index 0000000000..41aa73cc3b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushResult.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +/** + * Result of push operation to the remote repository. Holding information of + * {@link OperationResult} and remote refs updates status. + * + * @see Transport#push(org.eclipse.jgit.lib.ProgressMonitor, Collection) + */ +public class PushResult extends OperationResult { + private Map<String, RemoteRefUpdate> remoteUpdates = Collections.emptyMap(); + + /** + * Get status of remote refs updates. Together with + * {@link #getAdvertisedRefs()} it provides full description/status of each + * ref update. + * <p> + * Returned collection is not sorted in any order. + * </p> + * + * @return collection of remote refs updates + */ + public Collection<RemoteRefUpdate> getRemoteUpdates() { + return Collections.unmodifiableCollection(remoteUpdates.values()); + } + + /** + * Get status of specific remote ref update by remote ref name. Together + * with {@link #getAdvertisedRef(String)} it provide full description/status + * of this ref update. + * + * @param refName + * remote ref name + * @return status of remote ref update + */ + public RemoteRefUpdate getRemoteUpdate(final String refName) { + return remoteUpdates.get(refName); + } + + void setRemoteUpdates( + final Map<String, RemoteRefUpdate> remoteUpdates) { + this.remoteUpdates = remoteUpdates; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java new file mode 100644 index 0000000000..60ebeabd99 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; + +/** + * A command being processed by {@link ReceivePack}. + * <p> + * This command instance roughly translates to the server side representation of + * the {@link RemoteRefUpdate} created by the client. + */ +public class ReceiveCommand { + /** Type of operation requested. */ + public static enum Type { + /** Create a new ref; the ref must not already exist. */ + CREATE, + + /** + * Update an existing ref with a fast-forward update. + * <p> + * During a fast-forward update no changes will be lost; only new + * commits are inserted into the ref. + */ + UPDATE, + + /** + * Update an existing ref by potentially discarding objects. + * <p> + * The current value of the ref is not fully reachable from the new + * value of the ref, so a successful command may result in one or more + * objects becoming unreachable. + */ + UPDATE_NONFASTFORWARD, + + /** Delete an existing ref; the ref should already exist. */ + DELETE; + } + + /** Result of the update command. */ + public static enum Result { + /** The command has not yet been attempted by the server. */ + NOT_ATTEMPTED, + + /** The server is configured to deny creation of this ref. */ + REJECTED_NOCREATE, + + /** The server is configured to deny deletion of this ref. */ + REJECTED_NODELETE, + + /** The update is a non-fast-forward update and isn't permitted. */ + REJECTED_NONFASTFORWARD, + + /** The update affects <code>HEAD</code> and cannot be permitted. */ + REJECTED_CURRENT_BRANCH, + + /** + * One or more objects aren't in the repository. + * <p> + * This is severe indication of either repository corruption on the + * server side, or a bug in the client wherein the client did not supply + * all required objects during the pack transfer. + */ + REJECTED_MISSING_OBJECT, + + /** Other failure; see {@link ReceiveCommand#getMessage()}. */ + REJECTED_OTHER_REASON, + + /** The ref could not be locked and updated atomically; try again. */ + LOCK_FAILURE, + + /** The change was completed successfully. */ + OK; + } + + private final ObjectId oldId; + + private final ObjectId newId; + + private final String name; + + private Type type; + + private Ref ref; + + private Result status; + + private String message; + + /** + * Create a new command for {@link ReceivePack}. + * + * @param oldId + * the old object id; must not be null. Use + * {@link ObjectId#zeroId()} to indicate a ref creation. + * @param newId + * the new object id; must not be null. Use + * {@link ObjectId#zeroId()} to indicate a ref deletion. + * @param name + * name of the ref being affected. + */ + public ReceiveCommand(final ObjectId oldId, final ObjectId newId, + final String name) { + this.oldId = oldId; + this.newId = newId; + this.name = name; + + type = Type.UPDATE; + if (ObjectId.zeroId().equals(oldId)) + type = Type.CREATE; + if (ObjectId.zeroId().equals(newId)) + type = Type.DELETE; + status = Result.NOT_ATTEMPTED; + } + + /** @return the old value the client thinks the ref has. */ + public ObjectId getOldId() { + return oldId; + } + + /** @return the requested new value for this ref. */ + public ObjectId getNewId() { + return newId; + } + + /** @return the name of the ref being updated. */ + public String getRefName() { + return name; + } + + /** @return the type of this command; see {@link Type}. */ + public Type getType() { + return type; + } + + /** @return the ref, if this was advertised by the connection. */ + public Ref getRef() { + return ref; + } + + /** @return the current status code of this command. */ + public Result getResult() { + return status; + } + + /** @return the message associated with a failure status. */ + public String getMessage() { + return message; + } + + /** + * Set the status of this command. + * + * @param s + * the new status code for this command. + */ + public void setResult(final Result s) { + setResult(s, null); + } + + /** + * Set the status of this command. + * + * @param s + * new status code for this command. + * @param m + * optional message explaining the new status. + */ + public void setResult(final Result s, final String m) { + status = s; + message = m; + } + + void setRef(final Ref r) { + ref = r; + } + + void setType(final Type t) { + type = t; + } + + @Override + public String toString() { + return getType().name() + ": " + getOldId().name() + " " + + getNewId().name() + " " + getRefName(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java new file mode 100644 index 0000000000..26b66db403 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java @@ -0,0 +1,913 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.BufferedWriter; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.PackProtocolException; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PackLock; +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.lib.Config.SectionParser; +import org.eclipse.jgit.revwalk.ObjectWalk; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand.Result; +import org.eclipse.jgit.util.io.InterruptTimer; +import org.eclipse.jgit.util.io.TimeoutInputStream; +import org.eclipse.jgit.util.io.TimeoutOutputStream; + +/** + * Implements the server side of a push connection, receiving objects. + */ +public class ReceivePack { + static final String CAPABILITY_REPORT_STATUS = BasePackPushConnection.CAPABILITY_REPORT_STATUS; + + static final String CAPABILITY_DELETE_REFS = BasePackPushConnection.CAPABILITY_DELETE_REFS; + + static final String CAPABILITY_OFS_DELTA = BasePackPushConnection.CAPABILITY_OFS_DELTA; + + /** Database we write the stored objects into. */ + private final Repository db; + + /** Revision traversal support over {@link #db}. */ + private final RevWalk walk; + + /** Should an incoming transfer validate objects? */ + private boolean checkReceivedObjects; + + /** Should an incoming transfer permit create requests? */ + private boolean allowCreates; + + /** Should an incoming transfer permit delete requests? */ + private boolean allowDeletes; + + /** Should an incoming transfer permit non-fast-forward requests? */ + private boolean allowNonFastForwards; + + private boolean allowOfsDelta; + + /** Identity to record action as within the reflog. */ + private PersonIdent refLogIdent; + + /** Hook to validate the update commands before execution. */ + private PreReceiveHook preReceive; + + /** Hook to report on the commands after execution. */ + private PostReceiveHook postReceive; + + /** Timeout in seconds to wait for client interaction. */ + private int timeout; + + /** Timer to manage {@link #timeout}. */ + private InterruptTimer timer; + + private TimeoutInputStream timeoutIn; + + private InputStream rawIn; + + private OutputStream rawOut; + + private PacketLineIn pckIn; + + private PacketLineOut pckOut; + + private PrintWriter msgs; + + /** The refs we advertised as existing at the start of the connection. */ + private Map<String, Ref> refs; + + /** Capabilities requested by the client. */ + private Set<String> enabledCapablities; + + /** Commands to execute, as received by the client. */ + private List<ReceiveCommand> commands; + + /** An exception caught while unpacking and fsck'ing the objects. */ + private Throwable unpackError; + + /** if {@link #enabledCapablities} has {@link #CAPABILITY_REPORT_STATUS} */ + private boolean reportStatus; + + /** Lock around the received pack file, while updating refs. */ + private PackLock packLock; + + /** + * Create a new pack receive for an open repository. + * + * @param into + * the destination repository. + */ + public ReceivePack(final Repository into) { + db = into; + walk = new RevWalk(db); + + final ReceiveConfig cfg = db.getConfig().get(ReceiveConfig.KEY); + checkReceivedObjects = cfg.checkReceivedObjects; + allowCreates = cfg.allowCreates; + allowDeletes = cfg.allowDeletes; + allowNonFastForwards = cfg.allowNonFastForwards; + allowOfsDelta = cfg.allowOfsDelta; + preReceive = PreReceiveHook.NULL; + postReceive = PostReceiveHook.NULL; + } + + private static class ReceiveConfig { + static final SectionParser<ReceiveConfig> KEY = new SectionParser<ReceiveConfig>() { + public ReceiveConfig parse(final Config cfg) { + return new ReceiveConfig(cfg); + } + }; + + final boolean checkReceivedObjects; + + final boolean allowCreates; + + final boolean allowDeletes; + + final boolean allowNonFastForwards; + + final boolean allowOfsDelta; + + ReceiveConfig(final Config config) { + checkReceivedObjects = config.getBoolean("receive", "fsckobjects", + false); + allowCreates = true; + allowDeletes = !config.getBoolean("receive", "denydeletes", false); + allowNonFastForwards = !config.getBoolean("receive", + "denynonfastforwards", false); + allowOfsDelta = config.getBoolean("repack", "usedeltabaseoffset", + true); + } + } + + /** @return the repository this receive completes into. */ + public final Repository getRepository() { + return db; + } + + /** @return the RevWalk instance used by this connection. */ + public final RevWalk getRevWalk() { + return walk; + } + + /** @return all refs which were advertised to the client. */ + public final Map<String, Ref> getAdvertisedRefs() { + return refs; + } + + /** + * @return true if this instance will verify received objects are formatted + * correctly. Validating objects requires more CPU time on this side + * of the connection. + */ + public boolean isCheckReceivedObjects() { + return checkReceivedObjects; + } + + /** + * @param check + * true to enable checking received objects; false to assume all + * received objects are valid. + */ + public void setCheckReceivedObjects(final boolean check) { + checkReceivedObjects = check; + } + + /** @return true if the client can request refs to be created. */ + public boolean isAllowCreates() { + return allowCreates; + } + + /** + * @param canCreate + * true to permit create ref commands to be processed. + */ + public void setAllowCreates(final boolean canCreate) { + allowCreates = canCreate; + } + + /** @return true if the client can request refs to be deleted. */ + public boolean isAllowDeletes() { + return allowDeletes; + } + + /** + * @param canDelete + * true to permit delete ref commands to be processed. + */ + public void setAllowDeletes(final boolean canDelete) { + allowDeletes = canDelete; + } + + /** + * @return true if the client can request non-fast-forward updates of a ref, + * possibly making objects unreachable. + */ + public boolean isAllowNonFastForwards() { + return allowNonFastForwards; + } + + /** + * @param canRewind + * true to permit the client to ask for non-fast-forward updates + * of an existing ref. + */ + public void setAllowNonFastForwards(final boolean canRewind) { + allowNonFastForwards = canRewind; + } + + /** @return identity of the user making the changes in the reflog. */ + public PersonIdent getRefLogIdent() { + return refLogIdent; + } + + /** + * Set the identity of the user appearing in the affected reflogs. + * <p> + * The timestamp portion of the identity is ignored. A new identity with the + * current timestamp will be created automatically when the updates occur + * and the log records are written. + * + * @param pi + * identity of the user. If null the identity will be + * automatically determined based on the repository + * configuration. + */ + public void setRefLogIdent(final PersonIdent pi) { + refLogIdent = pi; + } + + /** @return get the hook invoked before updates occur. */ + public PreReceiveHook getPreReceiveHook() { + return preReceive; + } + + /** + * Set the hook which is invoked prior to commands being executed. + * <p> + * Only valid commands (those which have no obvious errors according to the + * received input and this instance's configuration) are passed into the + * hook. The hook may mark a command with a result of any value other than + * {@link Result#NOT_ATTEMPTED} to block its execution. + * <p> + * The hook may be called with an empty command collection if the current + * set is completely invalid. + * + * @param h + * the hook instance; may be null to disable the hook. + */ + public void setPreReceiveHook(final PreReceiveHook h) { + preReceive = h != null ? h : PreReceiveHook.NULL; + } + + /** @return get the hook invoked after updates occur. */ + public PostReceiveHook getPostReceiveHook() { + return postReceive; + } + + /** + * Set the hook which is invoked after commands are executed. + * <p> + * Only successful commands (type is {@link Result#OK}) are passed into the + * hook. The hook may be called with an empty command collection if the + * current set all resulted in an error. + * + * @param h + * the hook instance; may be null to disable the hook. + */ + public void setPostReceiveHook(final PostReceiveHook h) { + postReceive = h != null ? h : PostReceiveHook.NULL; + } + + /** @return timeout (in seconds) before aborting an IO operation. */ + public int getTimeout() { + return timeout; + } + + /** + * Set the timeout before willing to abort an IO call. + * + * @param seconds + * number of seconds to wait (with no data transfer occurring) + * before aborting an IO read or write operation with the + * connected client. + */ + public void setTimeout(final int seconds) { + timeout = seconds; + } + + /** @return all of the command received by the current request. */ + public List<ReceiveCommand> getAllCommands() { + return Collections.unmodifiableList(commands); + } + + /** + * Send an error message to the client, if it supports receiving them. + * <p> + * If the client doesn't support receiving messages, the message will be + * discarded, with no other indication to the caller or to the client. + * <p> + * {@link PreReceiveHook}s should always try to use + * {@link ReceiveCommand#setResult(Result, String)} with a result status of + * {@link Result#REJECTED_OTHER_REASON} to indicate any reasons for + * rejecting an update. Messages attached to a command are much more likely + * to be returned to the client. + * + * @param what + * string describing the problem identified by the hook. The + * string must not end with an LF, and must not contain an LF. + */ + public void sendError(final String what) { + sendMessage("error", what); + } + + /** + * Send a message to the client, if it supports receiving them. + * <p> + * If the client doesn't support receiving messages, the message will be + * discarded, with no other indication to the caller or to the client. + * + * @param what + * string describing the problem identified by the hook. The + * string must not end with an LF, and must not contain an LF. + */ + public void sendMessage(final String what) { + sendMessage("remote", what); + } + + private void sendMessage(final String type, final String what) { + if (msgs != null) + msgs.println(type + ": " + what); + } + + /** + * Execute the receive task on the socket. + * + * @param input + * raw input to read client commands and pack data from. Caller + * must ensure the input is buffered, otherwise read performance + * may suffer. + * @param output + * response back to the Git network client. Caller must ensure + * the output is buffered, otherwise write performance may + * suffer. + * @param messages + * secondary "notice" channel to send additional messages out + * through. When run over SSH this should be tied back to the + * standard error channel of the command execution. For most + * other network connections this should be null. + * @throws IOException + */ + public void receive(final InputStream input, final OutputStream output, + final OutputStream messages) throws IOException { + try { + rawIn = input; + rawOut = output; + + if (timeout > 0) { + final Thread caller = Thread.currentThread(); + timer = new InterruptTimer(caller.getName() + "-Timer"); + timeoutIn = new TimeoutInputStream(rawIn, timer); + TimeoutOutputStream o = new TimeoutOutputStream(rawOut, timer); + timeoutIn.setTimeout(timeout * 1000); + o.setTimeout(timeout * 1000); + rawIn = timeoutIn; + rawOut = o; + } + + pckIn = new PacketLineIn(rawIn); + pckOut = new PacketLineOut(rawOut); + if (messages != null) { + msgs = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(messages, Constants.CHARSET), + 8192)) { + @Override + public void println() { + print('\n'); + } + }; + } + + enabledCapablities = new HashSet<String>(); + commands = new ArrayList<ReceiveCommand>(); + + service(); + } finally { + try { + if (msgs != null) { + msgs.flush(); + } + } finally { + unlockPack(); + timeoutIn = null; + rawIn = null; + rawOut = null; + pckIn = null; + pckOut = null; + msgs = null; + refs = null; + enabledCapablities = null; + commands = null; + if (timer != null) { + try { + timer.terminate(); + } finally { + timer = null; + } + } + } + } + } + + private void service() throws IOException { + sendAdvertisedRefs(); + recvCommands(); + if (!commands.isEmpty()) { + enableCapabilities(); + + if (needPack()) { + try { + receivePack(); + if (isCheckReceivedObjects()) + checkConnectivity(); + unpackError = null; + } catch (IOException err) { + unpackError = err; + } catch (RuntimeException err) { + unpackError = err; + } catch (Error err) { + unpackError = err; + } + } + + if (unpackError == null) { + validateCommands(); + executeCommands(); + } + unlockPack(); + + if (reportStatus) { + sendStatusReport(true, new Reporter() { + void sendString(final String s) throws IOException { + pckOut.writeString(s + "\n"); + } + }); + pckOut.end(); + } else if (msgs != null) { + sendStatusReport(false, new Reporter() { + void sendString(final String s) throws IOException { + msgs.println(s); + } + }); + msgs.flush(); + } + + postReceive.onPostReceive(this, filterCommands(Result.OK)); + } + } + + private void unlockPack() { + if (packLock != null) { + packLock.unlock(); + packLock = null; + } + } + + private void sendAdvertisedRefs() throws IOException { + final RevFlag advertised = walk.newFlag("ADVERTISED"); + final RefAdvertiser adv = new RefAdvertiser(pckOut, walk, advertised); + adv.advertiseCapability(CAPABILITY_DELETE_REFS); + adv.advertiseCapability(CAPABILITY_REPORT_STATUS); + if (allowOfsDelta) + adv.advertiseCapability(CAPABILITY_OFS_DELTA); + refs = new HashMap<String, Ref>(db.getAllRefs()); + final Ref head = refs.remove(Constants.HEAD); + adv.send(refs.values()); + if (head != null && head.getName().equals(head.getOrigName())) + adv.advertiseHave(head.getObjectId()); + adv.includeAdditionalHaves(); + if (adv.isEmpty()) + adv.advertiseId(ObjectId.zeroId(), "capabilities^{}"); + pckOut.end(); + } + + private void recvCommands() throws IOException { + for (;;) { + String line; + try { + line = pckIn.readStringRaw(); + } catch (EOFException eof) { + if (commands.isEmpty()) + return; + throw eof; + } + if (line == PacketLineIn.END) + break; + + if (commands.isEmpty()) { + final int nul = line.indexOf('\0'); + if (nul >= 0) { + for (String c : line.substring(nul + 1).split(" ")) + enabledCapablities.add(c); + line = line.substring(0, nul); + } + } + + if (line.length() < 83) { + final String m = "error: invalid protocol: wanted 'old new ref'"; + sendError(m); + throw new PackProtocolException(m); + } + + final ObjectId oldId = ObjectId.fromString(line.substring(0, 40)); + final ObjectId newId = ObjectId.fromString(line.substring(41, 81)); + final String name = line.substring(82); + final ReceiveCommand cmd = new ReceiveCommand(oldId, newId, name); + cmd.setRef(refs.get(cmd.getRefName())); + commands.add(cmd); + } + } + + private void enableCapabilities() { + reportStatus = enabledCapablities.contains(CAPABILITY_REPORT_STATUS); + } + + private boolean needPack() { + for (final ReceiveCommand cmd : commands) { + if (cmd.getType() != ReceiveCommand.Type.DELETE) + return true; + } + return false; + } + + private void receivePack() throws IOException { + // It might take the client a while to pack the objects it needs + // to send to us. We should increase our timeout so we don't + // abort while the client is computing. + // + if (timeoutIn != null) + timeoutIn.setTimeout(10 * timeout * 1000); + + final IndexPack ip = IndexPack.create(db, rawIn); + ip.setFixThin(true); + ip.setObjectChecking(isCheckReceivedObjects()); + ip.index(NullProgressMonitor.INSTANCE); + + String lockMsg = "jgit receive-pack"; + if (getRefLogIdent() != null) + lockMsg += " from " + getRefLogIdent().toExternalString(); + packLock = ip.renameAndOpenPack(lockMsg); + + if (timeoutIn != null) + timeoutIn.setTimeout(timeout * 1000); + } + + private void checkConnectivity() throws IOException { + final ObjectWalk ow = new ObjectWalk(db); + for (final ReceiveCommand cmd : commands) { + if (cmd.getResult() != Result.NOT_ATTEMPTED) + continue; + if (cmd.getType() == ReceiveCommand.Type.DELETE) + continue; + ow.markStart(ow.parseAny(cmd.getNewId())); + } + for (final Ref ref : refs.values()) + ow.markUninteresting(ow.parseAny(ref.getObjectId())); + ow.checkConnectivity(); + } + + private void validateCommands() { + for (final ReceiveCommand cmd : commands) { + final Ref ref = cmd.getRef(); + if (cmd.getResult() != Result.NOT_ATTEMPTED) + continue; + + if (cmd.getType() == ReceiveCommand.Type.DELETE + && !isAllowDeletes()) { + // Deletes are not supported on this repository. + // + cmd.setResult(Result.REJECTED_NODELETE); + continue; + } + + if (cmd.getType() == ReceiveCommand.Type.CREATE) { + if (!isAllowCreates()) { + cmd.setResult(Result.REJECTED_NOCREATE); + continue; + } + + if (ref != null && !isAllowNonFastForwards()) { + // Creation over an existing ref is certainly not going + // to be a fast-forward update. We can reject it early. + // + cmd.setResult(Result.REJECTED_NONFASTFORWARD); + continue; + } + + if (ref != null) { + // A well behaved client shouldn't have sent us an + // update command for a ref we advertised to it. + // + cmd.setResult(Result.REJECTED_OTHER_REASON, "ref exists"); + continue; + } + } + + if (cmd.getType() == ReceiveCommand.Type.DELETE && ref != null + && !ObjectId.zeroId().equals(cmd.getOldId()) + && !ref.getObjectId().equals(cmd.getOldId())) { + // Delete commands can be sent with the old id matching our + // advertised value, *OR* with the old id being 0{40}. Any + // other requested old id is invalid. + // + cmd.setResult(Result.REJECTED_OTHER_REASON, + "invalid old id sent"); + continue; + } + + if (cmd.getType() == ReceiveCommand.Type.UPDATE) { + if (ref == null) { + // The ref must have been advertised in order to be updated. + // + cmd.setResult(Result.REJECTED_OTHER_REASON, "no such ref"); + continue; + } + + if (!ref.getObjectId().equals(cmd.getOldId())) { + // A properly functioning client will send the same + // object id we advertised. + // + cmd.setResult(Result.REJECTED_OTHER_REASON, + "invalid old id sent"); + continue; + } + + // Is this possibly a non-fast-forward style update? + // + RevObject oldObj, newObj; + try { + oldObj = walk.parseAny(cmd.getOldId()); + } catch (IOException e) { + cmd.setResult(Result.REJECTED_MISSING_OBJECT, cmd + .getOldId().name()); + continue; + } + + try { + newObj = walk.parseAny(cmd.getNewId()); + } catch (IOException e) { + cmd.setResult(Result.REJECTED_MISSING_OBJECT, cmd + .getNewId().name()); + continue; + } + + if (oldObj instanceof RevCommit && newObj instanceof RevCommit) { + try { + if (!walk.isMergedInto((RevCommit) oldObj, + (RevCommit) newObj)) { + cmd + .setType(ReceiveCommand.Type.UPDATE_NONFASTFORWARD); + } + } catch (MissingObjectException e) { + cmd.setResult(Result.REJECTED_MISSING_OBJECT, e + .getMessage()); + } catch (IOException e) { + cmd.setResult(Result.REJECTED_OTHER_REASON); + } + } else { + cmd.setType(ReceiveCommand.Type.UPDATE_NONFASTFORWARD); + } + } + + if (!cmd.getRefName().startsWith(Constants.R_REFS) + || !Repository.isValidRefName(cmd.getRefName())) { + cmd.setResult(Result.REJECTED_OTHER_REASON, "funny refname"); + } + } + } + + private void executeCommands() { + preReceive.onPreReceive(this, filterCommands(Result.NOT_ATTEMPTED)); + for (final ReceiveCommand cmd : filterCommands(Result.NOT_ATTEMPTED)) + execute(cmd); + } + + private void execute(final ReceiveCommand cmd) { + try { + final RefUpdate ru = db.updateRef(cmd.getRefName()); + ru.setRefLogIdent(getRefLogIdent()); + switch (cmd.getType()) { + case DELETE: + if (!ObjectId.zeroId().equals(cmd.getOldId())) { + // We can only do a CAS style delete if the client + // didn't bork its delete request by sending the + // wrong zero id rather than the advertised one. + // + ru.setExpectedOldObjectId(cmd.getOldId()); + } + ru.setForceUpdate(true); + status(cmd, ru.delete(walk)); + break; + + case CREATE: + case UPDATE: + case UPDATE_NONFASTFORWARD: + ru.setForceUpdate(isAllowNonFastForwards()); + ru.setExpectedOldObjectId(cmd.getOldId()); + ru.setNewObjectId(cmd.getNewId()); + ru.setRefLogMessage("push", true); + status(cmd, ru.update(walk)); + break; + } + } catch (IOException err) { + cmd.setResult(Result.REJECTED_OTHER_REASON, "lock error: " + + err.getMessage()); + } + } + + private void status(final ReceiveCommand cmd, final RefUpdate.Result result) { + switch (result) { + case NOT_ATTEMPTED: + cmd.setResult(Result.NOT_ATTEMPTED); + break; + + case LOCK_FAILURE: + case IO_FAILURE: + cmd.setResult(Result.LOCK_FAILURE); + break; + + case NO_CHANGE: + case NEW: + case FORCED: + case FAST_FORWARD: + cmd.setResult(Result.OK); + break; + + case REJECTED: + cmd.setResult(Result.REJECTED_NONFASTFORWARD); + break; + + case REJECTED_CURRENT_BRANCH: + cmd.setResult(Result.REJECTED_CURRENT_BRANCH); + break; + + default: + cmd.setResult(Result.REJECTED_OTHER_REASON, result.name()); + break; + } + } + + private List<ReceiveCommand> filterCommands(final Result want) { + final List<ReceiveCommand> r = new ArrayList<ReceiveCommand>(commands + .size()); + for (final ReceiveCommand cmd : commands) { + if (cmd.getResult() == want) + r.add(cmd); + } + return r; + } + + private void sendStatusReport(final boolean forClient, final Reporter out) + throws IOException { + if (unpackError != null) { + out.sendString("unpack error " + unpackError.getMessage()); + if (forClient) { + for (final ReceiveCommand cmd : commands) { + out.sendString("ng " + cmd.getRefName() + + " n/a (unpacker error)"); + } + } + return; + } + + if (forClient) + out.sendString("unpack ok"); + for (final ReceiveCommand cmd : commands) { + if (cmd.getResult() == Result.OK) { + if (forClient) + out.sendString("ok " + cmd.getRefName()); + continue; + } + + final StringBuilder r = new StringBuilder(); + r.append("ng "); + r.append(cmd.getRefName()); + r.append(" "); + + switch (cmd.getResult()) { + case NOT_ATTEMPTED: + r.append("server bug; ref not processed"); + break; + + case REJECTED_NOCREATE: + r.append("creation prohibited"); + break; + + case REJECTED_NODELETE: + r.append("deletion prohibited"); + break; + + case REJECTED_NONFASTFORWARD: + r.append("non-fast forward"); + break; + + case REJECTED_CURRENT_BRANCH: + r.append("branch is currently checked out"); + break; + + case REJECTED_MISSING_OBJECT: + if (cmd.getMessage() == null) + r.append("missing object(s)"); + else if (cmd.getMessage().length() == 2 * Constants.OBJECT_ID_LENGTH) + r.append("object " + cmd.getMessage() + " missing"); + else + r.append(cmd.getMessage()); + break; + + case REJECTED_OTHER_REASON: + if (cmd.getMessage() == null) + r.append("unspecified reason"); + else + r.append(cmd.getMessage()); + break; + + case LOCK_FAILURE: + r.append("failed to lock"); + break; + + case OK: + // We shouldn't have reached this case (see 'ok' case above). + continue; + } + out.sendString(r.toString()); + } + } + + static abstract class Reporter { + abstract void sendString(String s) throws IOException; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java new file mode 100644 index 0000000000..dfbd891b0b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.eclipse.jgit.lib.AlternateRepositoryDatabase; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectDatabase; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefComparator; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; + +/** Support for the start of {@link UploadPack} and {@link ReceivePack}. */ +class RefAdvertiser { + private final PacketLineOut pckOut; + + private final RevWalk walk; + + private final RevFlag ADVERTISED; + + private final StringBuilder tmpLine = new StringBuilder(100); + + private final char[] tmpId = new char[2 * Constants.OBJECT_ID_LENGTH]; + + private final Set<String> capablities = new LinkedHashSet<String>(); + + private boolean derefTags; + + private boolean first = true; + + RefAdvertiser(final PacketLineOut out, final RevWalk protoWalk, + final RevFlag advertisedFlag) { + pckOut = out; + walk = protoWalk; + ADVERTISED = advertisedFlag; + } + + void setDerefTags(final boolean deref) { + derefTags = deref; + } + + void advertiseCapability(String name) { + capablities.add(name); + } + + void send(final Collection<Ref> refs) throws IOException { + for (final Ref r : RefComparator.sort(refs)) { + final RevObject obj = parseAnyOrNull(r.getObjectId()); + if (obj != null) { + advertiseAny(obj, r.getOrigName()); + if (derefTags && obj instanceof RevTag) + advertiseTag((RevTag) obj, r.getOrigName() + "^{}"); + } + } + } + + void advertiseHave(AnyObjectId id) throws IOException { + RevObject obj = parseAnyOrNull(id); + if (obj != null) { + advertiseAnyOnce(obj, ".have"); + if (obj instanceof RevTag) + advertiseAnyOnce(((RevTag) obj).getObject(), ".have"); + } + } + + void includeAdditionalHaves() throws IOException { + additionalHaves(walk.getRepository().getObjectDatabase()); + } + + private void additionalHaves(final ObjectDatabase db) throws IOException { + if (db instanceof AlternateRepositoryDatabase) + additionalHaves(((AlternateRepositoryDatabase) db).getRepository()); + for (ObjectDatabase alt : db.getAlternates()) + additionalHaves(alt); + } + + private void additionalHaves(final Repository alt) throws IOException { + for (final Ref r : alt.getAllRefs().values()) + advertiseHave(r.getObjectId()); + } + + boolean isEmpty() { + return first; + } + + private RevObject parseAnyOrNull(final AnyObjectId id) { + if (id == null) + return null; + try { + return walk.parseAny(id); + } catch (IOException e) { + return null; + } + } + + private void advertiseAnyOnce(final RevObject obj, final String refName) + throws IOException { + if (!obj.has(ADVERTISED)) + advertiseAny(obj, refName); + } + + private void advertiseAny(final RevObject obj, final String refName) + throws IOException { + obj.add(ADVERTISED); + advertiseId(obj, refName); + } + + private void advertiseTag(final RevTag tag, final String refName) + throws IOException { + RevObject o = tag; + do { + // Fully unwrap here so later on we have these already parsed. + final RevObject target = ((RevTag) o).getObject(); + try { + walk.parseHeaders(target); + } catch (IOException err) { + return; + } + target.add(ADVERTISED); + o = target; + } while (o instanceof RevTag); + advertiseAny(tag.getObject(), refName); + } + + void advertiseId(final AnyObjectId id, final String refName) + throws IOException { + tmpLine.setLength(0); + id.copyTo(tmpId, tmpLine); + tmpLine.append(' '); + tmpLine.append(refName); + if (first) { + first = false; + if (!capablities.isEmpty()) { + tmpLine.append('\0'); + for (final String capName : capablities) { + tmpLine.append(' '); + tmpLine.append(capName); + } + tmpLine.append(' '); + } + } + tmpLine.append('\n'); + pckOut.writeString(tmpLine.toString()); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java new file mode 100644 index 0000000000..1949ef0878 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Ref; + +/** + * Describes how refs in one repository copy into another repository. + * <p> + * A ref specification provides matching support and limited rules to rewrite a + * reference in one repository to another reference in another repository. + */ +public class RefSpec { + /** + * Suffix for wildcard ref spec component, that indicate matching all refs + * with specified prefix. + */ + public static final String WILDCARD_SUFFIX = "/*"; + + /** + * Check whether provided string is a wildcard ref spec component. + * + * @param s + * ref spec component - string to test. Can be null. + * @return true if provided string is a wildcard ref spec component. + */ + public static boolean isWildcard(final String s) { + return s != null && s.endsWith(WILDCARD_SUFFIX); + } + + /** Does this specification ask for forced updated (rewind/reset)? */ + private boolean force; + + /** Is this specification actually a wildcard match? */ + private boolean wildcard; + + /** Name of the ref(s) we would copy from. */ + private String srcName; + + /** Name of the ref(s) we would copy into. */ + private String dstName; + + /** + * Construct an empty RefSpec. + * <p> + * A newly created empty RefSpec is not suitable for use in most + * applications, as at least one field must be set to match a source name. + */ + public RefSpec() { + force = false; + wildcard = false; + srcName = Constants.HEAD; + dstName = null; + } + + /** + * Parse a ref specification for use during transport operations. + * <p> + * Specifications are typically one of the following forms: + * <ul> + * <li><code>refs/head/master</code></li> + * <li><code>refs/head/master:refs/remotes/origin/master</code></li> + * <li><code>refs/head/*:refs/remotes/origin/*</code></li> + * <li><code>+refs/head/master</code></li> + * <li><code>+refs/head/master:refs/remotes/origin/master</code></li> + * <li><code>+refs/head/*:refs/remotes/origin/*</code></li> + * <li><code>:refs/head/master</code></li> + * </ul> + * + * @param spec + * string describing the specification. + * @throws IllegalArgumentException + * the specification is invalid. + */ + public RefSpec(final String spec) { + String s = spec; + if (s.startsWith("+")) { + force = true; + s = s.substring(1); + } + + final int c = s.lastIndexOf(':'); + if (c == 0) { + s = s.substring(1); + if (isWildcard(s)) + throw new IllegalArgumentException("Invalid wildcards " + spec); + dstName = s; + } else if (c > 0) { + srcName = s.substring(0, c); + dstName = s.substring(c + 1); + if (isWildcard(srcName) && isWildcard(dstName)) + wildcard = true; + else if (isWildcard(srcName) || isWildcard(dstName)) + throw new IllegalArgumentException("Invalid wildcards " + spec); + } else { + if (isWildcard(s)) + throw new IllegalArgumentException("Invalid wildcards " + spec); + srcName = s; + } + } + + private RefSpec(final RefSpec p) { + force = p.isForceUpdate(); + wildcard = p.isWildcard(); + srcName = p.getSource(); + dstName = p.getDestination(); + } + + /** + * Check if this specification wants to forcefully update the destination. + * + * @return true if this specification asks for updates without merge tests. + */ + public boolean isForceUpdate() { + return force; + } + + /** + * Create a new RefSpec with a different force update setting. + * + * @param forceUpdate + * new value for force update in the returned instance. + * @return a new RefSpec with force update as specified. + */ + public RefSpec setForceUpdate(final boolean forceUpdate) { + final RefSpec r = new RefSpec(this); + r.force = forceUpdate; + return r; + } + + /** + * Check if this specification is actually a wildcard pattern. + * <p> + * If this is a wildcard pattern then the source and destination names + * returned by {@link #getSource()} and {@link #getDestination()} will not + * be actual ref names, but instead will be patterns. + * + * @return true if this specification could match more than one ref. + */ + public boolean isWildcard() { + return wildcard; + } + + /** + * Get the source ref description. + * <p> + * During a fetch this is the name of the ref on the remote repository we + * are fetching from. During a push this is the name of the ref on the local + * repository we are pushing out from. + * + * @return name (or wildcard pattern) to match the source ref. + */ + public String getSource() { + return srcName; + } + + /** + * Create a new RefSpec with a different source name setting. + * + * @param source + * new value for source in the returned instance. + * @return a new RefSpec with source as specified. + * @throws IllegalStateException + * There is already a destination configured, and the wildcard + * status of the existing destination disagrees with the + * wildcard status of the new source. + */ + public RefSpec setSource(final String source) { + final RefSpec r = new RefSpec(this); + r.srcName = source; + if (isWildcard(r.srcName) && r.dstName == null) + throw new IllegalStateException("Destination is not a wildcard."); + if (isWildcard(r.srcName) != isWildcard(r.dstName)) + throw new IllegalStateException("Source/Destination must match."); + return r; + } + + /** + * Get the destination ref description. + * <p> + * During a fetch this is the local tracking branch that will be updated + * with the new ObjectId after fetching is complete. During a push this is + * the remote ref that will be updated by the remote's receive-pack process. + * <p> + * If null during a fetch no tracking branch should be updated and the + * ObjectId should be stored transiently in order to prepare a merge. + * <p> + * If null during a push, use {@link #getSource()} instead. + * + * @return name (or wildcard) pattern to match the destination ref. + */ + public String getDestination() { + return dstName; + } + + /** + * Create a new RefSpec with a different destination name setting. + * + * @param destination + * new value for destination in the returned instance. + * @return a new RefSpec with destination as specified. + * @throws IllegalStateException + * There is already a source configured, and the wildcard status + * of the existing source disagrees with the wildcard status of + * the new destination. + */ + public RefSpec setDestination(final String destination) { + final RefSpec r = new RefSpec(this); + r.dstName = destination; + if (isWildcard(r.dstName) && r.srcName == null) + throw new IllegalStateException("Source is not a wildcard."); + if (isWildcard(r.srcName) != isWildcard(r.dstName)) + throw new IllegalStateException("Source/Destination must match."); + return r; + } + + /** + * Create a new RefSpec with a different source/destination name setting. + * + * @param source + * new value for source in the returned instance. + * @param destination + * new value for destination in the returned instance. + * @return a new RefSpec with destination as specified. + * @throws IllegalArgumentException + * The wildcard status of the new source disagrees with the + * wildcard status of the new destination. + */ + public RefSpec setSourceDestination(final String source, + final String destination) { + if (isWildcard(source) != isWildcard(destination)) + throw new IllegalArgumentException("Source/Destination must match."); + final RefSpec r = new RefSpec(this); + r.wildcard = isWildcard(source); + r.srcName = source; + r.dstName = destination; + return r; + } + + /** + * Does this specification's source description match the ref name? + * + * @param r + * ref name that should be tested. + * @return true if the names match; false otherwise. + */ + public boolean matchSource(final String r) { + return match(r, getSource()); + } + + /** + * Does this specification's source description match the ref? + * + * @param r + * ref whose name should be tested. + * @return true if the names match; false otherwise. + */ + public boolean matchSource(final Ref r) { + return match(r.getName(), getSource()); + } + + /** + * Does this specification's destination description match the ref name? + * + * @param r + * ref name that should be tested. + * @return true if the names match; false otherwise. + */ + public boolean matchDestination(final String r) { + return match(r, getDestination()); + } + + /** + * Does this specification's destination description match the ref? + * + * @param r + * ref whose name should be tested. + * @return true if the names match; false otherwise. + */ + public boolean matchDestination(final Ref r) { + return match(r.getName(), getDestination()); + } + + /** + * Expand this specification to exactly match a ref name. + * <p> + * Callers must first verify the passed ref name matches this specification, + * otherwise expansion results may be unpredictable. + * + * @param r + * a ref name that matched our source specification. Could be a + * wildcard also. + * @return a new specification expanded from provided ref name. Result + * specification is wildcard if and only if provided ref name is + * wildcard. + */ + public RefSpec expandFromSource(final String r) { + return isWildcard() ? new RefSpec(this).expandFromSourceImp(r) : this; + } + + private RefSpec expandFromSourceImp(final String name) { + final String psrc = srcName, pdst = dstName; + wildcard = false; + srcName = name; + dstName = pdst.substring(0, pdst.length() - 1) + + name.substring(psrc.length() - 1); + return this; + } + + /** + * Expand this specification to exactly match a ref. + * <p> + * Callers must first verify the passed ref matches this specification, + * otherwise expansion results may be unpredictable. + * + * @param r + * a ref that matched our source specification. Could be a + * wildcard also. + * @return a new specification expanded from provided ref name. Result + * specification is wildcard if and only if provided ref name is + * wildcard. + */ + public RefSpec expandFromSource(final Ref r) { + return expandFromSource(r.getName()); + } + + /** + * Expand this specification to exactly match a ref name. + * <p> + * Callers must first verify the passed ref name matches this specification, + * otherwise expansion results may be unpredictable. + * + * @param r + * a ref name that matched our destination specification. Could + * be a wildcard also. + * @return a new specification expanded from provided ref name. Result + * specification is wildcard if and only if provided ref name is + * wildcard. + */ + public RefSpec expandFromDestination(final String r) { + return isWildcard() ? new RefSpec(this).expandFromDstImp(r) : this; + } + + private RefSpec expandFromDstImp(final String name) { + final String psrc = srcName, pdst = dstName; + wildcard = false; + srcName = psrc.substring(0, psrc.length() - 1) + + name.substring(pdst.length() - 1); + dstName = name; + return this; + } + + /** + * Expand this specification to exactly match a ref. + * <p> + * Callers must first verify the passed ref matches this specification, + * otherwise expansion results may be unpredictable. + * + * @param r + * a ref that matched our destination specification. + * @return a new specification expanded from provided ref name. Result + * specification is wildcard if and only if provided ref name is + * wildcard. + */ + public RefSpec expandFromDestination(final Ref r) { + return expandFromDestination(r.getName()); + } + + private boolean match(final String refName, final String s) { + if (s == null) + return false; + if (isWildcard()) + return refName.startsWith(s.substring(0, s.length() - 1)); + return refName.equals(s); + } + + public int hashCode() { + int hc = 0; + if (getSource() != null) + hc = hc * 31 + getSource().hashCode(); + if (getDestination() != null) + hc = hc * 31 + getDestination().hashCode(); + return hc; + } + + public boolean equals(final Object obj) { + if (!(obj instanceof RefSpec)) + return false; + final RefSpec b = (RefSpec) obj; + if (isForceUpdate() != b.isForceUpdate()) + return false; + if (isWildcard() != b.isWildcard()) + return false; + if (!eq(getSource(), b.getSource())) + return false; + if (!eq(getDestination(), b.getDestination())) + return false; + return true; + } + + private static boolean eq(final String a, final String b) { + if (a == b) + return true; + if (a == null || b == null) + return false; + return a.equals(b); + } + + public String toString() { + final StringBuilder r = new StringBuilder(); + if (isForceUpdate()) + r.append('+'); + if (getSource() != null) + r.append(getSource()); + if (getDestination() != null) { + r.append(':'); + r.append(getDestination()); + } + return r.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java new file mode 100644 index 0000000000..f05b8c6a30 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jgit.lib.Config; + +/** + * A remembered remote repository, including URLs and RefSpecs. + * <p> + * A remote configuration remembers one or more URLs for a frequently accessed + * remote repository as well as zero or more fetch and push specifications + * describing how refs should be transferred between this repository and the + * remote repository. + */ +public class RemoteConfig { + private static final String SECTION = "remote"; + + private static final String KEY_URL = "url"; + + private static final String KEY_PUSHURL = "pushurl"; + + private static final String KEY_FETCH = "fetch"; + + private static final String KEY_PUSH = "push"; + + private static final String KEY_UPLOADPACK = "uploadpack"; + + private static final String KEY_RECEIVEPACK = "receivepack"; + + private static final String KEY_TAGOPT = "tagopt"; + + private static final String KEY_MIRROR = "mirror"; + + private static final String KEY_TIMEOUT = "timeout"; + + private static final boolean DEFAULT_MIRROR = false; + + /** Default value for {@link #getUploadPack()} if not specified. */ + public static final String DEFAULT_UPLOAD_PACK = "git-upload-pack"; + + /** Default value for {@link #getReceivePack()} if not specified. */ + public static final String DEFAULT_RECEIVE_PACK = "git-receive-pack"; + + /** + * Parse all remote blocks in an existing configuration file, looking for + * remotes configuration. + * + * @param rc + * the existing configuration to get the remote settings from. + * The configuration must already be loaded into memory. + * @return all remotes configurations existing in provided repository + * configuration. Returned configurations are ordered + * lexicographically by names. + * @throws URISyntaxException + * one of the URIs within the remote's configuration is invalid. + */ + public static List<RemoteConfig> getAllRemoteConfigs(final Config rc) + throws URISyntaxException { + final List<String> names = new ArrayList<String>(rc + .getSubsections(SECTION)); + Collections.sort(names); + + final List<RemoteConfig> result = new ArrayList<RemoteConfig>(names + .size()); + for (final String name : names) + result.add(new RemoteConfig(rc, name)); + return result; + } + + private String name; + + private List<URIish> uris; + + private List<URIish> pushURIs; + + private List<RefSpec> fetch; + + private List<RefSpec> push; + + private String uploadpack; + + private String receivepack; + + private TagOpt tagopt; + + private boolean mirror; + + private int timeout; + + /** + * Parse a remote block from an existing configuration file. + * <p> + * This constructor succeeds even if the requested remote is not defined + * within the supplied configuration file. If that occurs then there will be + * no URIs and no ref specifications known to the new instance. + * + * @param rc + * the existing configuration to get the remote settings from. + * The configuration must already be loaded into memory. + * @param remoteName + * subsection key indicating the name of this remote. + * @throws URISyntaxException + * one of the URIs within the remote's configuration is invalid. + */ + public RemoteConfig(final Config rc, final String remoteName) + throws URISyntaxException { + name = remoteName; + + String[] vlst; + String val; + + vlst = rc.getStringList(SECTION, name, KEY_URL); + uris = new ArrayList<URIish>(vlst.length); + for (final String s : vlst) + uris.add(new URIish(s)); + + vlst = rc.getStringList(SECTION, name, KEY_PUSHURL); + pushURIs = new ArrayList<URIish>(vlst.length); + for (final String s : vlst) + pushURIs.add(new URIish(s)); + + vlst = rc.getStringList(SECTION, name, KEY_FETCH); + fetch = new ArrayList<RefSpec>(vlst.length); + for (final String s : vlst) + fetch.add(new RefSpec(s)); + + vlst = rc.getStringList(SECTION, name, KEY_PUSH); + push = new ArrayList<RefSpec>(vlst.length); + for (final String s : vlst) + push.add(new RefSpec(s)); + + val = rc.getString(SECTION, name, KEY_UPLOADPACK); + if (val == null) + val = DEFAULT_UPLOAD_PACK; + uploadpack = val; + + val = rc.getString(SECTION, name, KEY_RECEIVEPACK); + if (val == null) + val = DEFAULT_RECEIVE_PACK; + receivepack = val; + + val = rc.getString(SECTION, name, KEY_TAGOPT); + tagopt = TagOpt.fromOption(val); + mirror = rc.getBoolean(SECTION, name, KEY_MIRROR, DEFAULT_MIRROR); + timeout = rc.getInt(SECTION, name, KEY_TIMEOUT, 0); + } + + /** + * Update this remote's definition within the configuration. + * + * @param rc + * the configuration file to store ourselves into. + */ + public void update(final Config rc) { + final List<String> vlst = new ArrayList<String>(); + + vlst.clear(); + for (final URIish u : getURIs()) + vlst.add(u.toPrivateString()); + rc.setStringList(SECTION, getName(), KEY_URL, vlst); + + vlst.clear(); + for (final URIish u : getPushURIs()) + vlst.add(u.toPrivateString()); + rc.setStringList(SECTION, getName(), KEY_PUSHURL, vlst); + + vlst.clear(); + for (final RefSpec u : getFetchRefSpecs()) + vlst.add(u.toString()); + rc.setStringList(SECTION, getName(), KEY_FETCH, vlst); + + vlst.clear(); + for (final RefSpec u : getPushRefSpecs()) + vlst.add(u.toString()); + rc.setStringList(SECTION, getName(), KEY_PUSH, vlst); + + set(rc, KEY_UPLOADPACK, getUploadPack(), DEFAULT_UPLOAD_PACK); + set(rc, KEY_RECEIVEPACK, getReceivePack(), DEFAULT_RECEIVE_PACK); + set(rc, KEY_TAGOPT, getTagOpt().option(), TagOpt.AUTO_FOLLOW.option()); + set(rc, KEY_MIRROR, mirror, DEFAULT_MIRROR); + set(rc, KEY_TIMEOUT, timeout, 0); + } + + private void set(final Config rc, final String key, + final String currentValue, final String defaultValue) { + if (defaultValue.equals(currentValue)) + unset(rc, key); + else + rc.setString(SECTION, getName(), key, currentValue); + } + + private void set(final Config rc, final String key, + final boolean currentValue, final boolean defaultValue) { + if (defaultValue == currentValue) + unset(rc, key); + else + rc.setBoolean(SECTION, getName(), key, currentValue); + } + + private void set(final Config rc, final String key, final int currentValue, + final int defaultValue) { + if (defaultValue == currentValue) + unset(rc, key); + else + rc.setInt(SECTION, getName(), key, currentValue); + } + + private void unset(final Config rc, final String key) { + rc.unset(SECTION, getName(), key); + } + + /** + * Get the local name this remote configuration is recognized as. + * + * @return name assigned by the user to this configuration block. + */ + public String getName() { + return name; + } + + /** + * Get all configured URIs under this remote. + * + * @return the set of URIs known to this remote. + */ + public List<URIish> getURIs() { + return Collections.unmodifiableList(uris); + } + + /** + * Add a new URI to the end of the list of URIs. + * + * @param toAdd + * the new URI to add to this remote. + * @return true if the URI was added; false if it already exists. + */ + public boolean addURI(final URIish toAdd) { + if (uris.contains(toAdd)) + return false; + return uris.add(toAdd); + } + + /** + * Remove a URI from the list of URIs. + * + * @param toRemove + * the URI to remove from this remote. + * @return true if the URI was added; false if it already exists. + */ + public boolean removeURI(final URIish toRemove) { + return uris.remove(toRemove); + } + + /** + * Get all configured push-only URIs under this remote. + * + * @return the set of URIs known to this remote. + */ + public List<URIish> getPushURIs() { + return Collections.unmodifiableList(pushURIs); + } + + /** + * Add a new push-only URI to the end of the list of URIs. + * + * @param toAdd + * the new URI to add to this remote. + * @return true if the URI was added; false if it already exists. + */ + public boolean addPushURI(final URIish toAdd) { + if (pushURIs.contains(toAdd)) + return false; + return pushURIs.add(toAdd); + } + + /** + * Remove a push-only URI from the list of URIs. + * + * @param toRemove + * the URI to remove from this remote. + * @return true if the URI was added; false if it already exists. + */ + public boolean removePushURI(final URIish toRemove) { + return pushURIs.remove(toRemove); + } + + /** + * Remembered specifications for fetching from a repository. + * + * @return set of specs used by default when fetching. + */ + public List<RefSpec> getFetchRefSpecs() { + return Collections.unmodifiableList(fetch); + } + + /** + * Add a new fetch RefSpec to this remote. + * + * @param s + * the new specification to add. + * @return true if the specification was added; false if it already exists. + */ + public boolean addFetchRefSpec(final RefSpec s) { + if (fetch.contains(s)) + return false; + return fetch.add(s); + } + + /** + * Override existing fetch specifications with new ones. + * + * @param specs + * list of fetch specifications to set. List is copied, it can be + * modified after this call. + */ + public void setFetchRefSpecs(final List<RefSpec> specs) { + fetch.clear(); + fetch.addAll(specs); + } + + /** + * Override existing push specifications with new ones. + * + * @param specs + * list of push specifications to set. List is copied, it can be + * modified after this call. + */ + public void setPushRefSpecs(final List<RefSpec> specs) { + push.clear(); + push.addAll(specs); + } + + /** + * Remove a fetch RefSpec from this remote. + * + * @param s + * the specification to remove. + * @return true if the specification existed and was removed. + */ + public boolean removeFetchRefSpec(final RefSpec s) { + return fetch.remove(s); + } + + /** + * Remembered specifications for pushing to a repository. + * + * @return set of specs used by default when pushing. + */ + public List<RefSpec> getPushRefSpecs() { + return Collections.unmodifiableList(push); + } + + /** + * Add a new push RefSpec to this remote. + * + * @param s + * the new specification to add. + * @return true if the specification was added; false if it already exists. + */ + public boolean addPushRefSpec(final RefSpec s) { + if (push.contains(s)) + return false; + return push.add(s); + } + + /** + * Remove a push RefSpec from this remote. + * + * @param s + * the specification to remove. + * @return true if the specification existed and was removed. + */ + public boolean removePushRefSpec(final RefSpec s) { + return push.remove(s); + } + + /** + * Override for the location of 'git-upload-pack' on the remote system. + * <p> + * This value is only useful for an SSH style connection, where Git is + * asking the remote system to execute a program that provides the necessary + * network protocol. + * + * @return location of 'git-upload-pack' on the remote system. If no + * location has been configured the default of 'git-upload-pack' is + * returned instead. + */ + public String getUploadPack() { + return uploadpack; + } + + /** + * Override for the location of 'git-receive-pack' on the remote system. + * <p> + * This value is only useful for an SSH style connection, where Git is + * asking the remote system to execute a program that provides the necessary + * network protocol. + * + * @return location of 'git-receive-pack' on the remote system. If no + * location has been configured the default of 'git-receive-pack' is + * returned instead. + */ + public String getReceivePack() { + return receivepack; + } + + /** + * Get the description of how annotated tags should be treated during fetch. + * + * @return option indicating the behavior of annotated tags in fetch. + */ + public TagOpt getTagOpt() { + return tagopt; + } + + /** + * Set the description of how annotated tags should be treated on fetch. + * + * @param option + * method to use when handling annotated tags. + */ + public void setTagOpt(final TagOpt option) { + tagopt = option != null ? option : TagOpt.AUTO_FOLLOW; + } + + /** + * @return true if pushing to the remote automatically deletes remote refs + * which don't exist on the source side. + */ + public boolean isMirror() { + return mirror; + } + + /** + * Set the mirror flag to automatically delete remote refs. + * + * @param m + * true to automatically delete remote refs during push. + */ + public void setMirror(final boolean m) { + mirror = m; + } + + /** @return timeout (in seconds) before aborting an IO operation. */ + public int getTimeout() { + return timeout; + } + + /** + * Set the timeout before willing to abort an IO call. + * + * @param seconds + * number of seconds to wait (with no data transfer occurring) + * before aborting an IO read or write operation with this + * remote. A timeout of 0 will block indefinitely. + */ + public void setTimeout(final int seconds) { + timeout = seconds; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java new file mode 100644 index 0000000000..b2aa6335d8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Represent request and status of a remote ref update. Specification is + * provided by client, while status is handled by {@link PushProcess} class, + * being read-only for client. + * <p> + * Client can create instances of this class directly, basing on user + * specification and advertised refs ({@link Connection} or through + * {@link Transport} helper methods. Apply this specification on remote + * repository using + * {@link Transport#push(org.eclipse.jgit.lib.ProgressMonitor, java.util.Collection)} + * method. + * </p> + * + */ +public class RemoteRefUpdate { + /** + * Represent current status of a remote ref update. + */ + public static enum Status { + /** + * Push process hasn't yet attempted to update this ref. This is the + * default status, prior to push process execution. + */ + NOT_ATTEMPTED, + + /** + * Remote ref was up to date, there was no need to update anything. + */ + UP_TO_DATE, + + /** + * Remote ref update was rejected, as it would cause non fast-forward + * update. + */ + REJECTED_NONFASTFORWARD, + + /** + * Remote ref update was rejected, because remote side doesn't + * support/allow deleting refs. + */ + REJECTED_NODELETE, + + /** + * Remote ref update was rejected, because old object id on remote + * repository wasn't the same as defined expected old object. + */ + REJECTED_REMOTE_CHANGED, + + /** + * Remote ref update was rejected for other reason, possibly described + * in {@link RemoteRefUpdate#getMessage()}. + */ + REJECTED_OTHER_REASON, + + /** + * Remote ref didn't exist. Can occur on delete request of a non + * existing ref. + */ + NON_EXISTING, + + /** + * Push process is awaiting update report from remote repository. This + * is a temporary state or state after critical error in push process. + */ + AWAITING_REPORT, + + /** + * Remote ref was successfully updated. + */ + OK; + } + + private final ObjectId expectedOldObjectId; + + private final ObjectId newObjectId; + + private final String remoteName; + + private final TrackingRefUpdate trackingRefUpdate; + + private final String srcRef; + + private final boolean forceUpdate; + + private Status status; + + private boolean fastForward; + + private String message; + + private final Repository localDb; + + /** + * Construct remote ref update request by providing an update specification. + * Object is created with default {@link Status#NOT_ATTEMPTED} status and no + * message. + * + * @param localDb + * local repository to push from. + * @param srcRef + * source revision - any string resolvable by + * {@link Repository#resolve(String)}. This resolves to the new + * object that the caller want remote ref to be after update. Use + * null or {@link ObjectId#zeroId()} string for delete request. + * @param remoteName + * full name of a remote ref to update, e.g. "refs/heads/master" + * (no wildcard, no short name). + * @param forceUpdate + * true when caller want remote ref to be updated regardless + * whether it is fast-forward update (old object is ancestor of + * new object). + * @param localName + * optional full name of a local stored tracking branch, to + * update after push, e.g. "refs/remotes/zawir/dirty" (no + * wildcard, no short name); null if no local tracking branch + * should be updated. + * @param expectedOldObjectId + * optional object id that caller is expecting, requiring to be + * advertised by remote side before update; update will take + * place ONLY if remote side advertise exactly this expected id; + * null if caller doesn't care what object id remote side + * advertise. Use {@link ObjectId#zeroId()} when expecting no + * remote ref with this name. + * @throws IOException + * when I/O error occurred during creating + * {@link TrackingRefUpdate} for local tracking branch or srcRef + * can't be resolved to any object. + * @throws IllegalArgumentException + * if some required parameter was null + */ + public RemoteRefUpdate(final Repository localDb, final String srcRef, + final String remoteName, final boolean forceUpdate, + final String localName, final ObjectId expectedOldObjectId) + throws IOException { + if (remoteName == null) + throw new IllegalArgumentException("Remote name can't be null."); + this.srcRef = srcRef; + this.newObjectId = (srcRef == null ? ObjectId.zeroId() : localDb + .resolve(srcRef)); + if (newObjectId == null) + throw new IOException("Source ref " + srcRef + + " doesn't resolve to any object."); + this.remoteName = remoteName; + this.forceUpdate = forceUpdate; + if (localName != null && localDb != null) + trackingRefUpdate = new TrackingRefUpdate(localDb, localName, + remoteName, true, newObjectId, "push"); + else + trackingRefUpdate = null; + this.localDb = localDb; + this.expectedOldObjectId = expectedOldObjectId; + this.status = Status.NOT_ATTEMPTED; + } + + /** + * Create a new instance of this object basing on existing instance for + * configuration. State (like {@link #getMessage()}, {@link #getStatus()}) + * of base object is not shared. Expected old object id is set up from + * scratch, as this constructor may be used for 2-stage push: first one + * being dry run, second one being actual push. + * + * @param base + * configuration base. + * @param newExpectedOldObjectId + * new expected object id value. + * @throws IOException + * when I/O error occurred during creating + * {@link TrackingRefUpdate} for local tracking branch or srcRef + * of base object no longer can be resolved to any object. + */ + public RemoteRefUpdate(final RemoteRefUpdate base, + final ObjectId newExpectedOldObjectId) throws IOException { + this(base.localDb, base.srcRef, base.remoteName, base.forceUpdate, + (base.trackingRefUpdate == null ? null : base.trackingRefUpdate + .getLocalName()), newExpectedOldObjectId); + } + + /** + * @return expectedOldObjectId required to be advertised by remote side, as + * set in constructor; may be null. + */ + public ObjectId getExpectedOldObjectId() { + return expectedOldObjectId; + } + + /** + * @return true if some object is required to be advertised by remote side, + * as set in constructor; false otherwise. + */ + public boolean isExpectingOldObjectId() { + return expectedOldObjectId != null; + } + + /** + * @return newObjectId for remote ref, as set in constructor. + */ + public ObjectId getNewObjectId() { + return newObjectId; + } + + /** + * @return true if this update is deleting update; false otherwise. + */ + public boolean isDelete() { + return ObjectId.zeroId().equals(newObjectId); + } + + /** + * @return name of remote ref to update, as set in constructor. + */ + public String getRemoteName() { + return remoteName; + } + + /** + * @return local tracking branch update if localName was set in constructor. + */ + public TrackingRefUpdate getTrackingRefUpdate() { + return trackingRefUpdate; + } + + /** + * @return source revision as specified by user (in constructor), could be + * any string parseable by {@link Repository#resolve(String)}; can + * be null if specified that way in constructor - this stands for + * delete request. + */ + public String getSrcRef() { + return srcRef; + } + + /** + * @return true if user specified a local tracking branch for remote update; + * false otherwise. + */ + public boolean hasTrackingRefUpdate() { + return trackingRefUpdate != null; + } + + /** + * @return true if this update is forced regardless of old remote ref + * object; false otherwise. + */ + public boolean isForceUpdate() { + return forceUpdate; + } + + /** + * @return status of remote ref update operation. + */ + public Status getStatus() { + return status; + } + + /** + * Check whether update was fast-forward. Note that this result is + * meaningful only after successful update (when status is {@link Status#OK}). + * + * @return true if update was fast-forward; false otherwise. + */ + public boolean isFastForward() { + return fastForward; + } + + /** + * @return message describing reasons of status when needed/possible; may be + * null. + */ + public String getMessage() { + return message; + } + + void setStatus(final Status status) { + this.status = status; + } + + void setFastForward(boolean fastForward) { + this.fastForward = fastForward; + } + + void setMessage(final String message) { + this.message = message; + } + + /** + * Update locally stored tracking branch with the new object. + * + * @param walk + * walker used for checking update properties. + * @throws IOException + * when I/O error occurred during update + */ + protected void updateTrackingRef(final RevWalk walk) throws IOException { + if (isDelete()) + trackingRefUpdate.delete(walk); + else + trackingRefUpdate.update(walk); + } + + @Override + public String toString() { + return "RemoteRefUpdate[remoteName=" + remoteName + ", " + status + + ", " + (expectedOldObjectId!=null?expectedOldObjectId.abbreviate(localDb).name() :"(null)") + + "..." + (newObjectId != null ? newObjectId.abbreviate(localDb).name() : "(null)") + + (fastForward ? ", fastForward" : "") + + ", srcRef=" + srcRef + (forceUpdate ? ", forceUpdate" : "") + ", message=" + (message != null ? "\"" + + message + "\"" : "null") + ", " + localDb.getDirectory() + "]"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java new file mode 100644 index 0000000000..7a0765030f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jgit.errors.PackProtocolException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.util.NB; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * Unmultiplexes the data portion of a side-band channel. + * <p> + * Reading from this input stream obtains data from channel 1, which is + * typically the bulk data stream. + * <p> + * Channel 2 is transparently unpacked and "scraped" to update a progress + * monitor. The scraping is performed behind the scenes as part of any of the + * read methods offered by this stream. + * <p> + * Channel 3 results in an exception being thrown, as the remote side has issued + * an unrecoverable error. + * + * @see PacketLineIn#sideband(ProgressMonitor) + */ +class SideBandInputStream extends InputStream { + static final int CH_DATA = 1; + + static final int CH_PROGRESS = 2; + + static final int CH_ERROR = 3; + + private static Pattern P_UNBOUNDED = Pattern.compile( + "^([\\w ]+): (\\d+)( |, done)?.*", Pattern.DOTALL); + + private static Pattern P_BOUNDED = Pattern.compile( + "^([\\w ]+):.*\\((\\d+)/(\\d+)\\).*", Pattern.DOTALL); + + private final PacketLineIn pckIn; + + private final InputStream in; + + private final ProgressMonitor monitor; + + private String progressBuffer = ""; + + private String currentTask; + + private int lastCnt; + + private boolean eof; + + private int channel; + + private int available; + + SideBandInputStream(final PacketLineIn aPckIn, final InputStream aIn, + final ProgressMonitor aProgress) { + pckIn = aPckIn; + in = aIn; + monitor = aProgress; + currentTask = ""; + } + + @Override + public int read() throws IOException { + needDataPacket(); + if (eof) + return -1; + available--; + return in.read(); + } + + @Override + public int read(final byte[] b, int off, int len) throws IOException { + int r = 0; + while (len > 0) { + needDataPacket(); + if (eof) + break; + final int n = in.read(b, off, Math.min(len, available)); + if (n < 0) + break; + r += n; + off += n; + len -= n; + available -= n; + } + return eof && r == 0 ? -1 : r; + } + + private void needDataPacket() throws IOException { + if (eof || (channel == CH_DATA && available > 0)) + return; + for (;;) { + available = pckIn.readLength(); + if (available == 0) { + eof = true; + return; + } + + channel = in.read(); + available -= 5; // length header plus channel indicator + if (available == 0) + continue; + + switch (channel) { + case CH_DATA: + return; + case CH_PROGRESS: + progress(readString(available)); + + continue; + case CH_ERROR: + eof = true; + throw new TransportException("remote: " + readString(available)); + default: + throw new PackProtocolException("Invalid channel " + channel); + } + } + } + + private void progress(String pkt) { + pkt = progressBuffer + pkt; + for (;;) { + final int lf = pkt.indexOf('\n'); + final int cr = pkt.indexOf('\r'); + final int s; + if (0 <= lf && 0 <= cr) + s = Math.min(lf, cr); + else if (0 <= lf) + s = lf; + else if (0 <= cr) + s = cr; + else + break; + + final String msg = pkt.substring(0, s); + if (doProgressLine(msg)) + pkt = pkt.substring(s + 1); + else + break; + } + progressBuffer = pkt; + } + + private boolean doProgressLine(final String msg) { + Matcher matcher; + + matcher = P_BOUNDED.matcher(msg); + if (matcher.matches()) { + final String taskname = matcher.group(1); + if (!currentTask.equals(taskname)) { + currentTask = taskname; + lastCnt = 0; + final int tot = Integer.parseInt(matcher.group(3)); + monitor.beginTask(currentTask, tot); + } + final int cnt = Integer.parseInt(matcher.group(2)); + monitor.update(cnt - lastCnt); + lastCnt = cnt; + return true; + } + + matcher = P_UNBOUNDED.matcher(msg); + if (matcher.matches()) { + final String taskname = matcher.group(1); + if (!currentTask.equals(taskname)) { + currentTask = taskname; + lastCnt = 0; + monitor.beginTask(currentTask, ProgressMonitor.UNKNOWN); + } + final int cnt = Integer.parseInt(matcher.group(2)); + monitor.update(cnt - lastCnt); + lastCnt = cnt; + return true; + } + + return false; + } + + private String readString(final int len) throws IOException { + final byte[] raw = new byte[len]; + NB.readFully(in, raw, 0, len); + return RawParseUtils.decode(Constants.CHARSET, raw, 0, len); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandOutputStream.java new file mode 100644 index 0000000000..5e50fd89b3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandOutputStream.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Multiplexes data and progress messages + * <p> + * To correctly use this class you must wrap it in a BufferedOutputStream with a + * buffer size no larger than either {@link #SMALL_BUF} or {@link #MAX_BUF}, + * minus {@link #HDR_SIZE}. + */ +class SideBandOutputStream extends OutputStream { + static final int CH_DATA = SideBandInputStream.CH_DATA; + + static final int CH_PROGRESS = SideBandInputStream.CH_PROGRESS; + + static final int CH_ERROR = SideBandInputStream.CH_ERROR; + + static final int SMALL_BUF = 1000; + + static final int MAX_BUF = 65520; + + static final int HDR_SIZE = 5; + + private final int channel; + + private final PacketLineOut pckOut; + + private byte[] singleByteBuffer; + + SideBandOutputStream(final int chan, final PacketLineOut out) { + channel = chan; + pckOut = out; + } + + @Override + public void flush() throws IOException { + if (channel != CH_DATA) + pckOut.flush(); + } + + @Override + public void write(final byte[] b, final int off, final int len) + throws IOException { + pckOut.writeChannelPacket(channel, b, off, len); + } + + @Override + public void write(final int b) throws IOException { + if (singleByteBuffer == null) + singleByteBuffer = new byte[1]; + singleByteBuffer[0] = (byte) b; + write(singleByteBuffer); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandProgressMonitor.java new file mode 100644 index 0000000000..89d338c897 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandProgressMonitor.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.BufferedOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ProgressMonitor; + +/** Write progress messages out to the sideband channel. */ +class SideBandProgressMonitor implements ProgressMonitor { + private PrintWriter out; + + private boolean output; + + private long taskBeganAt; + + private long lastOutput; + + private String msg; + + private int lastWorked; + + private int totalWork; + + SideBandProgressMonitor(final PacketLineOut pckOut) { + final int bufsz = SideBandOutputStream.SMALL_BUF + - SideBandOutputStream.HDR_SIZE; + out = new PrintWriter(new OutputStreamWriter(new BufferedOutputStream( + new SideBandOutputStream(SideBandOutputStream.CH_PROGRESS, + pckOut), bufsz), Constants.CHARSET)); + } + + public void start(final int totalTasks) { + // Ignore the number of tasks. + taskBeganAt = System.currentTimeMillis(); + lastOutput = taskBeganAt; + } + + public void beginTask(final String title, final int total) { + endTask(); + msg = title; + lastWorked = 0; + totalWork = total; + } + + public void update(final int completed) { + if (msg == null) + return; + + final int cmp = lastWorked + completed; + final long now = System.currentTimeMillis(); + if (!output && now - taskBeganAt < 500) + return; + if (totalWork == UNKNOWN) { + if (now - lastOutput >= 500) { + display(cmp, null); + lastOutput = now; + } + } else { + if ((cmp * 100 / totalWork) != (lastWorked * 100) / totalWork + || now - lastOutput >= 500) { + display(cmp, null); + lastOutput = now; + } + } + lastWorked = cmp; + output = true; + } + + private void display(final int cmp, final String eol) { + final StringBuilder m = new StringBuilder(); + m.append(msg); + m.append(": "); + + if (totalWork == UNKNOWN) { + m.append(cmp); + } else { + final int pcnt = (cmp * 100 / totalWork); + if (pcnt < 100) + m.append(' '); + if (pcnt < 10) + m.append(' '); + m.append(pcnt); + m.append("% ("); + m.append(cmp); + m.append("/"); + m.append(totalWork); + m.append(")"); + } + if (eol != null) + m.append(eol); + else + m.append(" \r"); + out.print(m); + out.flush(); + } + + public boolean isCancelled() { + return false; + } + + public void endTask() { + if (output) { + if (totalWork == UNKNOWN) + display(lastWorked, ", done\n"); + else + display(totalWork, "\n"); + } + output = false; + msg = null; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConfigSessionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConfigSessionFactory.java new file mode 100644 index 0000000000..c30d32d9f9 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConfigSessionFactory.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2009, Google, Inc. + * Copyright (C) 2009, JetBrains s.r.o. + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jgit.util.FS; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import com.jcraft.jsch.UserInfo; + +/** + * The base session factory that loads known hosts and private keys from + * <code>$HOME/.ssh</code>. + * <p> + * This is the default implementation used by JGit and provides most of the + * compatibility necessary to match OpenSSH, a popular implementation of SSH + * used by C Git. + * <p> + * The factory does not provide UI behavior. Override the method + * {@link #configure(org.eclipse.jgit.transport.OpenSshConfig.Host, Session)} + * to supply appropriate {@link UserInfo} to the session. + */ +public abstract class SshConfigSessionFactory extends SshSessionFactory { + private final Map<String, JSch> byIdentityFile = new HashMap<String, JSch>(); + + private JSch defaultJSch; + + private OpenSshConfig config; + + @Override + public synchronized Session getSession(String user, String pass, + String host, int port) throws JSchException { + final OpenSshConfig.Host hc = getConfig().lookup(host); + host = hc.getHostName(); + if (port <= 0) + port = hc.getPort(); + if (user == null) + user = hc.getUser(); + + final Session session = createSession(hc, user, host, port); + if (pass != null) + session.setPassword(pass); + final String strictHostKeyCheckingPolicy = hc + .getStrictHostKeyChecking(); + if (strictHostKeyCheckingPolicy != null) + session.setConfig("StrictHostKeyChecking", + strictHostKeyCheckingPolicy); + final String pauth = hc.getPreferredAuthentications(); + if (pauth != null) + session.setConfig("PreferredAuthentications", pauth); + configure(hc, session); + return session; + } + + /** + * Create a new JSch session for the requested address. + * + * @param hc + * host configuration + * @param user + * login to authenticate as. + * @param host + * server name to connect to. + * @param port + * port number of the SSH daemon (typically 22). + * @return new session instance, but otherwise unconfigured. + * @throws JSchException + * the session could not be created. + */ + protected Session createSession(final OpenSshConfig.Host hc, + final String user, final String host, final int port) + throws JSchException { + return getJSch(hc).getSession(user, host, port); + } + + /** + * Provide additional configuration for the session based on the host + * information. This method could be used to supply {@link UserInfo}. + * + * @param hc + * host configuration + * @param session + * session to configure + */ + protected abstract void configure(OpenSshConfig.Host hc, Session session); + + /** + * Obtain the JSch used to create new sessions. + * + * @param hc + * host configuration + * @return the JSch instance to use. + * @throws JSchException + * the user configuration could not be created. + */ + protected JSch getJSch(final OpenSshConfig.Host hc) throws JSchException { + final JSch def = getDefaultJSch(); + final File identityFile = hc.getIdentityFile(); + if (identityFile == null) { + return def; + } + + final String identityKey = identityFile.getAbsolutePath(); + JSch jsch = byIdentityFile.get(identityKey); + if (jsch == null) { + jsch = new JSch(); + jsch.setHostKeyRepository(def.getHostKeyRepository()); + jsch.addIdentity(identityKey); + byIdentityFile.put(identityKey, jsch); + } + return jsch; + } + + private JSch getDefaultJSch() throws JSchException { + if (defaultJSch == null) { + defaultJSch = createDefaultJSch(); + for (Object name : defaultJSch.getIdentityNames()) { + byIdentityFile.put((String) name, defaultJSch); + } + } + return defaultJSch; + } + + /** + * @return the new default JSch implementation. + * @throws JSchException + * known host keys cannot be loaded. + */ + protected JSch createDefaultJSch() throws JSchException { + final JSch jsch = new JSch(); + knownHosts(jsch); + identities(jsch); + return jsch; + } + + private OpenSshConfig getConfig() { + if (config == null) + config = OpenSshConfig.get(); + return config; + } + + private static void knownHosts(final JSch sch) throws JSchException { + final File home = FS.userHome(); + if (home == null) + return; + final File known_hosts = new File(new File(home, ".ssh"), "known_hosts"); + try { + final FileInputStream in = new FileInputStream(known_hosts); + try { + sch.setKnownHosts(in); + } finally { + in.close(); + } + } catch (FileNotFoundException none) { + // Oh well. They don't have a known hosts in home. + } catch (IOException err) { + // Oh well. They don't have a known hosts in home. + } + } + + private static void identities(final JSch sch) { + final File home = FS.userHome(); + if (home == null) + return; + final File sshdir = new File(home, ".ssh"); + if (sshdir.isDirectory()) { + loadIdentity(sch, new File(sshdir, "identity")); + loadIdentity(sch, new File(sshdir, "id_rsa")); + loadIdentity(sch, new File(sshdir, "id_dsa")); + } + } + + private static void loadIdentity(final JSch sch, final File priv) { + if (priv.isFile()) { + try { + sch.addIdentity(priv.getAbsolutePath()); + } catch (JSchException e) { + // Instead, pretend the key doesn't exist. + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java new file mode 100644 index 0000000000..76bf6c1dcf --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; + +/** + * Creates and destroys SSH connections to a remote system. + * <p> + * Different implementations of the session factory may be used to control + * communicating with the end-user as well as reading their personal SSH + * configuration settings, such as known hosts and private keys. + * <p> + * A {@link Session} must be returned to the factory that created it. Callers + * are encouraged to retain the SshSessionFactory for the duration of the period + * they are using the Session. + */ +public abstract class SshSessionFactory { + private static SshSessionFactory INSTANCE = new DefaultSshSessionFactory(); + + /** + * Get the currently configured JVM-wide factory. + * <p> + * A factory is always available. By default the factory will read from the + * user's <code>$HOME/.ssh</code> and assume OpenSSH compatibility. + * + * @return factory the current factory for this JVM. + */ + public static SshSessionFactory getInstance() { + return INSTANCE; + } + + /** + * Change the JVM-wide factory to a different implementation. + * + * @param newFactory + * factory for future sessions to be created through. If null the + * default factory will be restored.s + */ + public static void setInstance(final SshSessionFactory newFactory) { + if (newFactory != null) + INSTANCE = newFactory; + else + INSTANCE = new DefaultSshSessionFactory(); + } + + /** + * Open (or reuse) a session to a host. + * <p> + * A reasonable UserInfo that can interact with the end-user (if necessary) + * is installed on the returned session by this method. + * <p> + * The caller must connect the session by invoking <code>connect()</code> + * if it has not already been connected. + * + * @param user + * username to authenticate as. If null a reasonable default must + * be selected by the implementation. This may be + * <code>System.getProperty("user.name")</code>. + * @param pass + * optional user account password or passphrase. If not null a + * UserInfo that supplies this value to the SSH library will be + * configured. + * @param host + * hostname (or IP address) to connect to. Must not be null. + * @param port + * port number the server is listening for connections on. May be <= + * 0 to indicate the IANA registered port of 22 should be used. + * @return a session that can contact the remote host. + * @throws JSchException + * the session could not be created. + */ + public abstract Session getSession(String user, String pass, String host, + int port) throws JSchException; + + /** + * Close (or recycle) a session to a host. + * + * @param session + * a session previously obtained from this factory's + * {@link #getSession(String,String, String, int)} method.s + */ + public void releaseSession(final Session session) { + if (session.isConnected()) + session.disconnect(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshTransport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshTransport.java new file mode 100644 index 0000000000..5c6b498cad --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshTransport.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2009, JetBrains s.r.o. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008-2009, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.net.ConnectException; +import java.net.UnknownHostException; + +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Repository; + +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; + +/** + * The base class for transports that use SSH protocol. This class allows + * customizing SSH connection settings. + */ +public abstract class SshTransport extends TcpTransport { + + private SshSessionFactory sch; + + /** + * The open SSH session + */ + protected Session sock; + + /** + * Create a new transport instance. + * + * @param local + * the repository this instance will fetch into, or push out of. + * This must be the repository passed to + * {@link #open(Repository, URIish)}. + * @param uri + * the URI used to access the remote repository. This must be the + * URI passed to {@link #open(Repository, URIish)}. + */ + protected SshTransport(Repository local, URIish uri) { + super(local, uri); + sch = SshSessionFactory.getInstance(); + } + + /** + * Set SSH session factory instead of the default one for this instance of + * the transport. + * + * @param factory + * a factory to set, must not be null + * @throws IllegalStateException + * if session has been already created. + */ + public void setSshSessionFactory(SshSessionFactory factory) { + if (factory == null) + throw new NullPointerException("The factory must not be null"); + if (sock != null) + throw new IllegalStateException( + "An SSH session has been already created"); + sch = factory; + } + + /** + * @return the SSH session factory that will be used for creating SSH sessions + */ + public SshSessionFactory getSshSessionFactory() { + return sch; + } + + + /** + * Initialize SSH session + * + * @throws TransportException + * in case of error with opening SSH session + */ + protected void initSession() throws TransportException { + if (sock != null) + return; + + final int tms = getTimeout() > 0 ? getTimeout() * 1000 : 0; + final String user = uri.getUser(); + final String pass = uri.getPass(); + final String host = uri.getHost(); + final int port = uri.getPort(); + try { + sock = sch.getSession(user, pass, host, port); + if (!sock.isConnected()) + sock.connect(tms); + } catch (JSchException je) { + final Throwable c = je.getCause(); + if (c instanceof UnknownHostException) + throw new TransportException(uri, "unknown host"); + if (c instanceof ConnectException) + throw new TransportException(uri, c.getMessage()); + throw new TransportException(uri, je.getMessage(), je); + } + } + + @Override + public void close() { + if (sock != null) { + try { + sch.releaseSession(sock); + } finally { + sock = null; + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TagOpt.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TagOpt.java new file mode 100644 index 0000000000..09cd56a729 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TagOpt.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2008, Mike Ralphson <mike@abacus.co.uk> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +/** Specification of annotated tag behavior during fetch. */ +public enum TagOpt { + /** + * Automatically follow tags if we fetch the thing they point at. + * <p> + * This is the default behavior and tries to balance the benefit of having + * an annotated tag against the cost of possibly objects that are only on + * branches we care nothing about. Annotated tags are fetched only if we can + * prove that we already have (or will have when the fetch completes) the + * object the annotated tag peels (dereferences) to. + */ + AUTO_FOLLOW(""), + + /** + * Never fetch tags, even if we have the thing it points at. + * <p> + * This option must be requested by the user and always avoids fetching + * annotated tags. It is most useful if the location you are fetching from + * publishes annotated tags, but you are not interested in the tags and only + * want their branches. + */ + NO_TAGS("--no-tags"), + + /** + * Always fetch tags, even if we do not have the thing it points at. + * <p> + * Unlike {@link #AUTO_FOLLOW} the tag is always obtained. This may cause + * hundreds of megabytes of objects to be fetched if the receiving + * repository does not yet have the necessary dependencies. + */ + FETCH_TAGS("--tags"); + + private final String option; + + private TagOpt(final String o) { + option = o; + } + + /** + * Get the command line/configuration file text for this value. + * + * @return text that appears in the configuration file to activate this. + */ + public String option() { + return option; + } + + /** + * Convert a command line/configuration file text into a value instance. + * + * @param o + * the configuration file text value. + * @return the option that matches the passed parameter. + */ + public static TagOpt fromOption(final String o) { + if (o == null || o.length() == 0) + return AUTO_FOLLOW; + for (final TagOpt tagopt : values()) { + if (tagopt.option().equals(o)) + return tagopt; + } + throw new IllegalArgumentException("Invalid tag option: " + o); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TcpTransport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TcpTransport.java new file mode 100644 index 0000000000..a6e5390890 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TcpTransport.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2009, JetBrains s.r.o. + * Copyright (C) 2009, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import org.eclipse.jgit.lib.Repository; + +/** + * The base class for transports based on TCP sockets. This class + * holds settings common for all TCP based transports. + */ +public abstract class TcpTransport extends Transport { + /** + * Create a new transport instance. + * + * @param local + * the repository this instance will fetch into, or push out of. + * This must be the repository passed to + * {@link #open(Repository, URIish)}. + * @param uri + * the URI used to access the remote repository. This must be the + * URI passed to {@link #open(Repository, URIish)}. + */ + protected TcpTransport(Repository local, URIish uri) { + super(local, uri); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java new file mode 100644 index 0000000000..2655f39f01 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.revwalk.RevWalk; + +/** Update of a locally stored tracking branch. */ +public class TrackingRefUpdate { + private final String remoteName; + + private final RefUpdate update; + + TrackingRefUpdate(final Repository db, final RefSpec spec, + final AnyObjectId nv, final String msg) throws IOException { + this(db, spec.getDestination(), spec.getSource(), spec.isForceUpdate(), + nv, msg); + } + + TrackingRefUpdate(final Repository db, final String localName, + final String remoteName, final boolean forceUpdate, + final AnyObjectId nv, final String msg) throws IOException { + this.remoteName = remoteName; + update = db.updateRef(localName); + update.setForceUpdate(forceUpdate); + update.setNewObjectId(nv); + update.setRefLogMessage(msg, true); + } + + /** + * Get the name of the remote ref. + * <p> + * Usually this is of the form "refs/heads/master". + * + * @return the name used within the remote repository. + */ + public String getRemoteName() { + return remoteName; + } + + /** + * Get the name of the local tracking ref. + * <p> + * Usually this is of the form "refs/remotes/origin/master". + * + * @return the name used within this local repository. + */ + public String getLocalName() { + return update.getName(); + } + + /** + * Get the new value the ref will be (or was) updated to. + * + * @return new value. Null if the caller has not configured it. + */ + public ObjectId getNewObjectId() { + return update.getNewObjectId(); + } + + /** + * The old value of the ref, prior to the update being attempted. + * <p> + * This value may differ before and after the update method. Initially it is + * populated with the value of the ref before the lock is taken, but the old + * value may change if someone else modified the ref between the time we + * last read it and when the ref was locked for update. + * + * @return the value of the ref prior to the update being attempted; null if + * the updated has not been attempted yet. + */ + public ObjectId getOldObjectId() { + return update.getOldObjectId(); + } + + /** + * Get the status of this update. + * + * @return the status of the update. + */ + public Result getResult() { + return update.getResult(); + } + + void update(final RevWalk walk) throws IOException { + update.update(walk); + } + + void delete(final RevWalk walk) throws IOException { + update.delete(walk); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java new file mode 100644 index 0000000000..e63afaf084 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java @@ -0,0 +1,932 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.TransferConfig; + +/** + * Connects two Git repositories together and copies objects between them. + * <p> + * A transport can be used for either fetching (copying objects into the + * caller's repository from the remote repository) or pushing (copying objects + * into the remote repository from the caller's repository). Each transport + * implementation is responsible for the details associated with establishing + * the network connection(s) necessary for the copy, as well as actually + * shuffling data back and forth. + * <p> + * Transport instances and the connections they create are not thread-safe. + * Callers must ensure a transport is accessed by only one thread at a time. + */ +public abstract class Transport { + /** Type of operation a Transport is being opened for. */ + public enum Operation { + /** Transport is to fetch objects locally. */ + FETCH, + /** Transport is to push objects remotely. */ + PUSH; + } + + /** + * Open a new transport instance to connect two repositories. + * <p> + * This method assumes {@link Operation#FETCH}. + * + * @param local + * existing local repository. + * @param remote + * location of the remote repository - may be URI or remote + * configuration name. + * @return the new transport instance. Never null. In case of multiple URIs + * in remote configuration, only the first is chosen. + * @throws URISyntaxException + * the location is not a remote defined in the configuration + * file and is not a well-formed URL. + * @throws NotSupportedException + * the protocol specified is not supported. + */ + public static Transport open(final Repository local, final String remote) + throws NotSupportedException, URISyntaxException { + return open(local, remote, Operation.FETCH); + } + + /** + * Open a new transport instance to connect two repositories. + * + * @param local + * existing local repository. + * @param remote + * location of the remote repository - may be URI or remote + * configuration name. + * @param op + * planned use of the returned Transport; the URI may differ + * based on the type of connection desired. + * @return the new transport instance. Never null. In case of multiple URIs + * in remote configuration, only the first is chosen. + * @throws URISyntaxException + * the location is not a remote defined in the configuration + * file and is not a well-formed URL. + * @throws NotSupportedException + * the protocol specified is not supported. + */ + public static Transport open(final Repository local, final String remote, + final Operation op) throws NotSupportedException, + URISyntaxException { + final RemoteConfig cfg = new RemoteConfig(local.getConfig(), remote); + if (doesNotExist(cfg)) + return open(local, new URIish(remote)); + return open(local, cfg, op); + } + + /** + * Open new transport instances to connect two repositories. + * <p> + * This method assumes {@link Operation#FETCH}. + * + * @param local + * existing local repository. + * @param remote + * location of the remote repository - may be URI or remote + * configuration name. + * @return the list of new transport instances for every URI in remote + * configuration. + * @throws URISyntaxException + * the location is not a remote defined in the configuration + * file and is not a well-formed URL. + * @throws NotSupportedException + * the protocol specified is not supported. + */ + public static List<Transport> openAll(final Repository local, + final String remote) throws NotSupportedException, + URISyntaxException { + return openAll(local, remote, Operation.FETCH); + } + + /** + * Open new transport instances to connect two repositories. + * + * @param local + * existing local repository. + * @param remote + * location of the remote repository - may be URI or remote + * configuration name. + * @param op + * planned use of the returned Transport; the URI may differ + * based on the type of connection desired. + * @return the list of new transport instances for every URI in remote + * configuration. + * @throws URISyntaxException + * the location is not a remote defined in the configuration + * file and is not a well-formed URL. + * @throws NotSupportedException + * the protocol specified is not supported. + */ + public static List<Transport> openAll(final Repository local, + final String remote, final Operation op) + throws NotSupportedException, URISyntaxException { + final RemoteConfig cfg = new RemoteConfig(local.getConfig(), remote); + if (doesNotExist(cfg)) { + final ArrayList<Transport> transports = new ArrayList<Transport>(1); + transports.add(open(local, new URIish(remote))); + return transports; + } + return openAll(local, cfg, op); + } + + /** + * Open a new transport instance to connect two repositories. + * <p> + * This method assumes {@link Operation#FETCH}. + * + * @param local + * existing local repository. + * @param cfg + * configuration describing how to connect to the remote + * repository. + * @return the new transport instance. Never null. In case of multiple URIs + * in remote configuration, only the first is chosen. + * @throws NotSupportedException + * the protocol specified is not supported. + * @throws IllegalArgumentException + * if provided remote configuration doesn't have any URI + * associated. + */ + public static Transport open(final Repository local, final RemoteConfig cfg) + throws NotSupportedException { + return open(local, cfg, Operation.FETCH); + } + + /** + * Open a new transport instance to connect two repositories. + * + * @param local + * existing local repository. + * @param cfg + * configuration describing how to connect to the remote + * repository. + * @param op + * planned use of the returned Transport; the URI may differ + * based on the type of connection desired. + * @return the new transport instance. Never null. In case of multiple URIs + * in remote configuration, only the first is chosen. + * @throws NotSupportedException + * the protocol specified is not supported. + * @throws IllegalArgumentException + * if provided remote configuration doesn't have any URI + * associated. + */ + public static Transport open(final Repository local, + final RemoteConfig cfg, final Operation op) + throws NotSupportedException { + final List<URIish> uris = getURIs(cfg, op); + if (uris.isEmpty()) + throw new IllegalArgumentException( + "Remote config \"" + + cfg.getName() + "\" has no URIs associated"); + final Transport tn = open(local, uris.get(0)); + tn.applyConfig(cfg); + return tn; + } + + /** + * Open new transport instances to connect two repositories. + * <p> + * This method assumes {@link Operation#FETCH}. + * + * @param local + * existing local repository. + * @param cfg + * configuration describing how to connect to the remote + * repository. + * @return the list of new transport instances for every URI in remote + * configuration. + * @throws NotSupportedException + * the protocol specified is not supported. + */ + public static List<Transport> openAll(final Repository local, + final RemoteConfig cfg) throws NotSupportedException { + return openAll(local, cfg, Operation.FETCH); + } + + /** + * Open new transport instances to connect two repositories. + * + * @param local + * existing local repository. + * @param cfg + * configuration describing how to connect to the remote + * repository. + * @param op + * planned use of the returned Transport; the URI may differ + * based on the type of connection desired. + * @return the list of new transport instances for every URI in remote + * configuration. + * @throws NotSupportedException + * the protocol specified is not supported. + */ + public static List<Transport> openAll(final Repository local, + final RemoteConfig cfg, final Operation op) + throws NotSupportedException { + final List<URIish> uris = getURIs(cfg, op); + final List<Transport> transports = new ArrayList<Transport>(uris.size()); + for (final URIish uri : uris) { + final Transport tn = open(local, uri); + tn.applyConfig(cfg); + transports.add(tn); + } + return transports; + } + + private static List<URIish> getURIs(final RemoteConfig cfg, + final Operation op) { + switch (op) { + case FETCH: + return cfg.getURIs(); + case PUSH: { + List<URIish> uris = cfg.getPushURIs(); + if (uris.isEmpty()) + uris = cfg.getURIs(); + return uris; + } + default: + throw new IllegalArgumentException(op.toString()); + } + } + + private static boolean doesNotExist(final RemoteConfig cfg) { + return cfg.getURIs().isEmpty() && cfg.getPushURIs().isEmpty(); + } + + /** + * Open a new transport instance to connect two repositories. + * + * @param local + * existing local repository. + * @param remote + * location of the remote repository. + * @return the new transport instance. Never null. + * @throws NotSupportedException + * the protocol specified is not supported. + */ + public static Transport open(final Repository local, final URIish remote) + throws NotSupportedException { + if (TransportGitSsh.canHandle(remote)) + return new TransportGitSsh(local, remote); + + else if (TransportHttp.canHandle(remote)) + return new TransportHttp(local, remote); + + else if (TransportSftp.canHandle(remote)) + return new TransportSftp(local, remote); + + else if (TransportGitAnon.canHandle(remote)) + return new TransportGitAnon(local, remote); + + else if (TransportAmazonS3.canHandle(remote)) + return new TransportAmazonS3(local, remote); + + else if (TransportBundleFile.canHandle(remote)) + return new TransportBundleFile(local, remote); + + else if (TransportLocal.canHandle(remote)) + return new TransportLocal(local, remote); + + throw new NotSupportedException("URI not supported: " + remote); + } + + /** + * Convert push remote refs update specification from {@link RefSpec} form + * to {@link RemoteRefUpdate}. Conversion expands wildcards by matching + * source part to local refs. expectedOldObjectId in RemoteRefUpdate is + * always set as null. Tracking branch is configured if RefSpec destination + * matches source of any fetch ref spec for this transport remote + * configuration. + * + * @param db + * local database. + * @param specs + * collection of RefSpec to convert. + * @param fetchSpecs + * fetch specifications used for finding localtracking refs. May + * be null or empty collection. + * @return collection of set up {@link RemoteRefUpdate}. + * @throws IOException + * when problem occurred during conversion or specification set + * up: most probably, missing objects or refs. + */ + public static Collection<RemoteRefUpdate> findRemoteRefUpdatesFor( + final Repository db, final Collection<RefSpec> specs, + Collection<RefSpec> fetchSpecs) throws IOException { + if (fetchSpecs == null) + fetchSpecs = Collections.emptyList(); + final List<RemoteRefUpdate> result = new LinkedList<RemoteRefUpdate>(); + final Collection<RefSpec> procRefs = expandPushWildcardsFor(db, specs); + + for (final RefSpec spec : procRefs) { + String srcSpec = spec.getSource(); + final Ref srcRef = db.getRef(srcSpec); + if (srcRef != null) + srcSpec = srcRef.getName(); + + String destSpec = spec.getDestination(); + if (destSpec == null) { + // No destination (no-colon in ref-spec), DWIMery assumes src + // + destSpec = srcSpec; + } + + if (srcRef != null && !destSpec.startsWith(Constants.R_REFS)) { + // Assume the same kind of ref at the destination, e.g. + // "refs/heads/foo:master", DWIMery assumes master is also + // under "refs/heads/". + // + final String n = srcRef.getName(); + final int kindEnd = n.indexOf('/', Constants.R_REFS.length()); + destSpec = n.substring(0, kindEnd + 1) + destSpec; + } + + final boolean forceUpdate = spec.isForceUpdate(); + final String localName = findTrackingRefName(destSpec, fetchSpecs); + final RemoteRefUpdate rru = new RemoteRefUpdate(db, srcSpec, + destSpec, forceUpdate, localName, null); + result.add(rru); + } + return result; + } + + private static Collection<RefSpec> expandPushWildcardsFor( + final Repository db, final Collection<RefSpec> specs) { + final Map<String, Ref> localRefs = db.getAllRefs(); + final Collection<RefSpec> procRefs = new HashSet<RefSpec>(); + + for (final RefSpec spec : specs) { + if (spec.isWildcard()) { + for (final Ref localRef : localRefs.values()) { + if (spec.matchSource(localRef)) + procRefs.add(spec.expandFromSource(localRef)); + } + } else { + procRefs.add(spec); + } + } + return procRefs; + } + + private static String findTrackingRefName(final String remoteName, + final Collection<RefSpec> fetchSpecs) { + // try to find matching tracking refs + for (final RefSpec fetchSpec : fetchSpecs) { + if (fetchSpec.matchSource(remoteName)) { + if (fetchSpec.isWildcard()) + return fetchSpec.expandFromSource(remoteName) + .getDestination(); + else + return fetchSpec.getDestination(); + } + } + return null; + } + + /** + * Default setting for {@link #fetchThin} option. + */ + public static final boolean DEFAULT_FETCH_THIN = true; + + /** + * Default setting for {@link #pushThin} option. + */ + public static final boolean DEFAULT_PUSH_THIN = false; + + /** + * Specification for fetch or push operations, to fetch or push all tags. + * Acts as --tags. + */ + public static final RefSpec REFSPEC_TAGS = new RefSpec( + "refs/tags/*:refs/tags/*"); + + /** + * Specification for push operation, to push all refs under refs/heads. Acts + * as --all. + */ + public static final RefSpec REFSPEC_PUSH_ALL = new RefSpec( + "refs/heads/*:refs/heads/*"); + + /** The repository this transport fetches into, or pushes out of. */ + protected final Repository local; + + /** The URI used to create this transport. */ + protected final URIish uri; + + /** Name of the upload pack program, if it must be executed. */ + private String optionUploadPack = RemoteConfig.DEFAULT_UPLOAD_PACK; + + /** Specifications to apply during fetch. */ + private List<RefSpec> fetch = Collections.emptyList(); + + /** + * How {@link #fetch(ProgressMonitor, Collection)} should handle tags. + * <p> + * We default to {@link TagOpt#NO_TAGS} so as to avoid fetching annotated + * tags during one-shot fetches used for later merges. This prevents + * dragging down tags from repositories that we do not have established + * tracking branches for. If we do not track the source repository, we most + * likely do not care about any tags it publishes. + */ + private TagOpt tagopt = TagOpt.NO_TAGS; + + /** Should fetch request thin-pack if remote repository can produce it. */ + private boolean fetchThin = DEFAULT_FETCH_THIN; + + /** Name of the receive pack program, if it must be executed. */ + private String optionReceivePack = RemoteConfig.DEFAULT_RECEIVE_PACK; + + /** Specifications to apply during push. */ + private List<RefSpec> push = Collections.emptyList(); + + /** Should push produce thin-pack when sending objects to remote repository. */ + private boolean pushThin = DEFAULT_PUSH_THIN; + + /** Should push just check for operation result, not really push. */ + private boolean dryRun; + + /** Should an incoming (fetch) transfer validate objects? */ + private boolean checkFetchedObjects; + + /** Should refs no longer on the source be pruned from the destination? */ + private boolean removeDeletedRefs; + + /** Timeout in seconds to wait before aborting an IO read or write. */ + private int timeout; + + /** + * Create a new transport instance. + * + * @param local + * the repository this instance will fetch into, or push out of. + * This must be the repository passed to + * {@link #open(Repository, URIish)}. + * @param uri + * the URI used to access the remote repository. This must be the + * URI passed to {@link #open(Repository, URIish)}. + */ + protected Transport(final Repository local, final URIish uri) { + final TransferConfig tc = local.getConfig().getTransfer(); + this.local = local; + this.uri = uri; + this.checkFetchedObjects = tc.isFsckObjects(); + } + + /** + * Get the URI this transport connects to. + * <p> + * Each transport instance connects to at most one URI at any point in time. + * + * @return the URI describing the location of the remote repository. + */ + public URIish getURI() { + return uri; + } + + /** + * Get the name of the remote executable providing upload-pack service. + * + * @return typically "git-upload-pack". + */ + public String getOptionUploadPack() { + return optionUploadPack; + } + + /** + * Set the name of the remote executable providing upload-pack services. + * + * @param where + * name of the executable. + */ + public void setOptionUploadPack(final String where) { + if (where != null && where.length() > 0) + optionUploadPack = where; + else + optionUploadPack = RemoteConfig.DEFAULT_UPLOAD_PACK; + } + + /** + * Get the description of how annotated tags should be treated during fetch. + * + * @return option indicating the behavior of annotated tags in fetch. + */ + public TagOpt getTagOpt() { + return tagopt; + } + + /** + * Set the description of how annotated tags should be treated on fetch. + * + * @param option + * method to use when handling annotated tags. + */ + public void setTagOpt(final TagOpt option) { + tagopt = option != null ? option : TagOpt.AUTO_FOLLOW; + } + + /** + * Default setting is: {@link #DEFAULT_FETCH_THIN} + * + * @return true if fetch should request thin-pack when possible; false + * otherwise + * @see PackTransport + */ + public boolean isFetchThin() { + return fetchThin; + } + + /** + * Set the thin-pack preference for fetch operation. Default setting is: + * {@link #DEFAULT_FETCH_THIN} + * + * @param fetchThin + * true when fetch should request thin-pack when possible; false + * when it shouldn't + * @see PackTransport + */ + public void setFetchThin(final boolean fetchThin) { + this.fetchThin = fetchThin; + } + + /** + * @return true if fetch will verify received objects are formatted + * correctly. Validating objects requires more CPU time on the + * client side of the connection. + */ + public boolean isCheckFetchedObjects() { + return checkFetchedObjects; + } + + /** + * @param check + * true to enable checking received objects; false to assume all + * received objects are valid. + */ + public void setCheckFetchedObjects(final boolean check) { + checkFetchedObjects = check; + } + + /** + * Default setting is: {@link RemoteConfig#DEFAULT_RECEIVE_PACK} + * + * @return remote executable providing receive-pack service for pack + * transports. + * @see PackTransport + */ + public String getOptionReceivePack() { + return optionReceivePack; + } + + /** + * Set remote executable providing receive-pack service for pack transports. + * Default setting is: {@link RemoteConfig#DEFAULT_RECEIVE_PACK} + * + * @param optionReceivePack + * remote executable, if null or empty default one is set; + */ + public void setOptionReceivePack(String optionReceivePack) { + if (optionReceivePack != null && optionReceivePack.length() > 0) + this.optionReceivePack = optionReceivePack; + else + this.optionReceivePack = RemoteConfig.DEFAULT_RECEIVE_PACK; + } + + /** + * Default setting is: {@value #DEFAULT_PUSH_THIN} + * + * @return true if push should produce thin-pack in pack transports + * @see PackTransport + */ + public boolean isPushThin() { + return pushThin; + } + + /** + * Set thin-pack preference for push operation. Default setting is: + * {@value #DEFAULT_PUSH_THIN} + * + * @param pushThin + * true when push should produce thin-pack in pack transports; + * false when it shouldn't + * @see PackTransport + */ + public void setPushThin(final boolean pushThin) { + this.pushThin = pushThin; + } + + /** + * @return true if destination refs should be removed if they no longer + * exist at the source repository. + */ + public boolean isRemoveDeletedRefs() { + return removeDeletedRefs; + } + + /** + * Set whether or not to remove refs which no longer exist in the source. + * <p> + * If true, refs at the destination repository (local for fetch, remote for + * push) are deleted if they no longer exist on the source side (remote for + * fetch, local for push). + * <p> + * False by default, as this may cause data to become unreachable, and + * eventually be deleted on the next GC. + * + * @param remove true to remove refs that no longer exist. + */ + public void setRemoveDeletedRefs(final boolean remove) { + removeDeletedRefs = remove; + } + + /** + * Apply provided remote configuration on this transport. + * + * @param cfg + * configuration to apply on this transport. + */ + public void applyConfig(final RemoteConfig cfg) { + setOptionUploadPack(cfg.getUploadPack()); + setOptionReceivePack(cfg.getReceivePack()); + setTagOpt(cfg.getTagOpt()); + fetch = cfg.getFetchRefSpecs(); + push = cfg.getPushRefSpecs(); + timeout = cfg.getTimeout(); + } + + /** + * @return true if push operation should just check for possible result and + * not really update remote refs, false otherwise - when push should + * act normally. + */ + public boolean isDryRun() { + return dryRun; + } + + /** + * Set dry run option for push operation. + * + * @param dryRun + * true if push operation should just check for possible result + * and not really update remote refs, false otherwise - when push + * should act normally. + */ + public void setDryRun(final boolean dryRun) { + this.dryRun = dryRun; + } + + /** @return timeout (in seconds) before aborting an IO operation. */ + public int getTimeout() { + return timeout; + } + + /** + * Set the timeout before willing to abort an IO call. + * + * @param seconds + * number of seconds to wait (with no data transfer occurring) + * before aborting an IO read or write operation with this + * remote. + */ + public void setTimeout(final int seconds) { + timeout = seconds; + } + + /** + * Fetch objects and refs from the remote repository to the local one. + * <p> + * This is a utility function providing standard fetch behavior. Local + * tracking refs associated with the remote repository are automatically + * updated if this transport was created from a {@link RemoteConfig} with + * fetch RefSpecs defined. + * + * @param monitor + * progress monitor to inform the user about our processing + * activity. Must not be null. Use {@link NullProgressMonitor} if + * progress updates are not interesting or necessary. + * @param toFetch + * specification of refs to fetch locally. May be null or the + * empty collection to use the specifications from the + * RemoteConfig. Source for each RefSpec can't be null. + * @return information describing the tracking refs updated. + * @throws NotSupportedException + * this transport implementation does not support fetching + * objects. + * @throws TransportException + * the remote connection could not be established or object + * copying (if necessary) failed or update specification was + * incorrect. + */ + public FetchResult fetch(final ProgressMonitor monitor, + Collection<RefSpec> toFetch) throws NotSupportedException, + TransportException { + if (toFetch == null || toFetch.isEmpty()) { + // If the caller did not ask for anything use the defaults. + // + if (fetch.isEmpty()) + throw new TransportException("Nothing to fetch."); + toFetch = fetch; + } else if (!fetch.isEmpty()) { + // If the caller asked for something specific without giving + // us the local tracking branch see if we can update any of + // the local tracking branches without incurring additional + // object transfer overheads. + // + final Collection<RefSpec> tmp = new ArrayList<RefSpec>(toFetch); + for (final RefSpec requested : toFetch) { + final String reqSrc = requested.getSource(); + for (final RefSpec configured : fetch) { + final String cfgSrc = configured.getSource(); + final String cfgDst = configured.getDestination(); + if (cfgSrc.equals(reqSrc) && cfgDst != null) { + tmp.add(configured); + break; + } + } + } + toFetch = tmp; + } + + final FetchResult result = new FetchResult(); + new FetchProcess(this, toFetch).execute(monitor, result); + return result; + } + + /** + * Push objects and refs from the local repository to the remote one. + * <p> + * This is a utility function providing standard push behavior. It updates + * remote refs and send there necessary objects according to remote ref + * update specification. After successful remote ref update, associated + * locally stored tracking branch is updated if set up accordingly. Detailed + * operation result is provided after execution. + * <p> + * For setting up remote ref update specification from ref spec, see helper + * method {@link #findRemoteRefUpdatesFor(Collection)}, predefined refspecs + * ({@link #REFSPEC_TAGS}, {@link #REFSPEC_PUSH_ALL}) or consider using + * directly {@link RemoteRefUpdate} for more possibilities. + * <p> + * When {@link #isDryRun()} is true, result of this operation is just + * estimation of real operation result, no real action is performed. + * + * @see RemoteRefUpdate + * + * @param monitor + * progress monitor to inform the user about our processing + * activity. Must not be null. Use {@link NullProgressMonitor} if + * progress updates are not interesting or necessary. + * @param toPush + * specification of refs to push. May be null or the empty + * collection to use the specifications from the RemoteConfig + * converted by {@link #findRemoteRefUpdatesFor(Collection)}. No + * more than 1 RemoteRefUpdate with the same remoteName is + * allowed. These objects are modified during this call. + * @return information about results of remote refs updates, tracking refs + * updates and refs advertised by remote repository. + * @throws NotSupportedException + * this transport implementation does not support pushing + * objects. + * @throws TransportException + * the remote connection could not be established or object + * copying (if necessary) failed at I/O or protocol level or + * update specification was incorrect. + */ + public PushResult push(final ProgressMonitor monitor, + Collection<RemoteRefUpdate> toPush) throws NotSupportedException, + TransportException { + if (toPush == null || toPush.isEmpty()) { + // If the caller did not ask for anything use the defaults. + try { + toPush = findRemoteRefUpdatesFor(push); + } catch (final IOException e) { + throw new TransportException( + "Problem with resolving push ref specs locally: " + + e.getMessage(), e); + } + if (toPush.isEmpty()) + throw new TransportException("Nothing to push."); + } + final PushProcess pushProcess = new PushProcess(this, toPush); + return pushProcess.execute(monitor); + } + + /** + * Convert push remote refs update specification from {@link RefSpec} form + * to {@link RemoteRefUpdate}. Conversion expands wildcards by matching + * source part to local refs. expectedOldObjectId in RemoteRefUpdate is + * always set as null. Tracking branch is configured if RefSpec destination + * matches source of any fetch ref spec for this transport remote + * configuration. + * <p> + * Conversion is performed for context of this transport (database, fetch + * specifications). + * + * @param specs + * collection of RefSpec to convert. + * @return collection of set up {@link RemoteRefUpdate}. + * @throws IOException + * when problem occurred during conversion or specification set + * up: most probably, missing objects or refs. + */ + public Collection<RemoteRefUpdate> findRemoteRefUpdatesFor( + final Collection<RefSpec> specs) throws IOException { + return findRemoteRefUpdatesFor(local, specs, fetch); + } + + /** + * Begins a new connection for fetching from the remote repository. + * + * @return a fresh connection to fetch from the remote repository. + * @throws NotSupportedException + * the implementation does not support fetching. + * @throws TransportException + * the remote connection could not be established. + */ + public abstract FetchConnection openFetch() throws NotSupportedException, + TransportException; + + /** + * Begins a new connection for pushing into the remote repository. + * + * @return a fresh connection to push into the remote repository. + * @throws NotSupportedException + * the implementation does not support pushing. + * @throws TransportException + * the remote connection could not be established + */ + public abstract PushConnection openPush() throws NotSupportedException, + TransportException; + + /** + * Close any resources used by this transport. + * <p> + * If the remote repository is contacted by a network socket this method + * must close that network socket, disconnecting the two peers. If the + * remote repository is actually local (same system) this method must close + * any open file handles used to read the "remote" repository. + */ + public abstract void close(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java new file mode 100644 index 0000000000..6a1a17f605 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; + +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Ref.Storage; +import org.eclipse.jgit.util.FS; + +/** + * Transport over the non-Git aware Amazon S3 protocol. + * <p> + * This transport communicates with the Amazon S3 servers (a non-free commercial + * hosting service that users must subscribe to). Some users may find transport + * to and from S3 to be a useful backup service. + * <p> + * The transport does not require any specialized Git support on the remote + * (server side) repository, as Amazon does not provide any such support. + * Repository files are retrieved directly through the S3 API, which uses + * extended HTTP/1.1 semantics. This make it possible to read or write Git data + * from a remote repository that is stored on S3. + * <p> + * Unlike the HTTP variant (see {@link TransportHttp}) we rely upon being able + * to list objects in a bucket, as the S3 API supports this function. By listing + * the bucket contents we can avoid relying on <code>objects/info/packs</code> + * or <code>info/refs</code> in the remote repository. + * <p> + * Concurrent pushing over this transport is not supported. Multiple concurrent + * push operations may cause confusion in the repository state. + * + * @see WalkFetchConnection + * @see WalkPushConnection + */ +public class TransportAmazonS3 extends HttpTransport implements WalkTransport { + static final String S3_SCHEME = "amazon-s3"; + + static boolean canHandle(final URIish uri) { + if (!uri.isRemote()) + return false; + return S3_SCHEME.equals(uri.getScheme()); + } + + /** User information necessary to connect to S3. */ + private final AmazonS3 s3; + + /** Bucket the remote repository is stored in. */ + private final String bucket; + + /** + * Key prefix which all objects related to the repository start with. + * <p> + * The prefix does not start with "/". + * <p> + * The prefix does not end with "/". The trailing slash is stripped during + * the constructor if a trailing slash was supplied in the URIish. + * <p> + * All files within the remote repository start with + * <code>keyPrefix + "/"</code>. + */ + private final String keyPrefix; + + TransportAmazonS3(final Repository local, final URIish uri) + throws NotSupportedException { + super(local, uri); + + Properties props = null; + File propsFile = new File(local.getDirectory(), uri.getUser()); + if (!propsFile.isFile()) + propsFile = new File(FS.userHome(), uri.getUser()); + if (propsFile.isFile()) { + try { + props = AmazonS3.properties(propsFile); + } catch (IOException e) { + throw new NotSupportedException("cannot read " + propsFile, e); + } + } else { + props = new Properties(); + props.setProperty("accesskey", uri.getUser()); + props.setProperty("secretkey", uri.getPass()); + } + + s3 = new AmazonS3(props); + bucket = uri.getHost(); + + String p = uri.getPath(); + if (p.startsWith("/")) + p = p.substring(1); + if (p.endsWith("/")) + p = p.substring(0, p.length() - 1); + keyPrefix = p; + } + + @Override + public FetchConnection openFetch() throws TransportException { + final DatabaseS3 c = new DatabaseS3(bucket, keyPrefix + "/objects"); + final WalkFetchConnection r = new WalkFetchConnection(this, c); + r.available(c.readAdvertisedRefs()); + return r; + } + + @Override + public PushConnection openPush() throws TransportException { + final DatabaseS3 c = new DatabaseS3(bucket, keyPrefix + "/objects"); + final WalkPushConnection r = new WalkPushConnection(this, c); + r.available(c.readAdvertisedRefs()); + return r; + } + + @Override + public void close() { + // No explicit connections are maintained. + } + + class DatabaseS3 extends WalkRemoteObjectDatabase { + private final String bucketName; + + private final String objectsKey; + + DatabaseS3(final String b, final String o) { + bucketName = b; + objectsKey = o; + } + + private String resolveKey(String subpath) { + if (subpath.endsWith("/")) + subpath = subpath.substring(0, subpath.length() - 1); + String k = objectsKey; + while (subpath.startsWith(ROOT_DIR)) { + k = k.substring(0, k.lastIndexOf('/')); + subpath = subpath.substring(3); + } + return k + "/" + subpath; + } + + @Override + URIish getURI() { + URIish u = new URIish(); + u = u.setScheme(S3_SCHEME); + u = u.setHost(bucketName); + u = u.setPath("/" + objectsKey); + return u; + } + + @Override + Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException { + try { + return readAlternates(INFO_ALTERNATES); + } catch (FileNotFoundException err) { + // Fall through. + } + return null; + } + + @Override + WalkRemoteObjectDatabase openAlternate(final String location) + throws IOException { + return new DatabaseS3(bucketName, resolveKey(location)); + } + + @Override + Collection<String> getPackNames() throws IOException { + final HashSet<String> have = new HashSet<String>(); + have.addAll(s3.list(bucket, resolveKey("pack"))); + + final Collection<String> packs = new ArrayList<String>(); + for (final String n : have) { + if (!n.startsWith("pack-") || !n.endsWith(".pack")) + continue; + + final String in = n.substring(0, n.length() - 5) + ".idx"; + if (have.contains(in)) + packs.add(n); + } + return packs; + } + + @Override + FileStream open(final String path) throws IOException { + final URLConnection c = s3.get(bucket, resolveKey(path)); + final InputStream raw = c.getInputStream(); + final InputStream in = s3.decrypt(c); + final int len = c.getContentLength(); + return new FileStream(in, raw == in ? len : -1); + } + + @Override + void deleteFile(final String path) throws IOException { + s3.delete(bucket, resolveKey(path)); + } + + @Override + OutputStream writeFile(final String path, + final ProgressMonitor monitor, final String monitorTask) + throws IOException { + return s3.beginPut(bucket, resolveKey(path), monitor, monitorTask); + } + + @Override + void writeFile(final String path, final byte[] data) throws IOException { + s3.put(bucket, resolveKey(path), data); + } + + Map<String, Ref> readAdvertisedRefs() throws TransportException { + final TreeMap<String, Ref> avail = new TreeMap<String, Ref>(); + readPackedRefs(avail); + readLooseRefs(avail); + readRef(avail, Constants.HEAD); + return avail; + } + + private void readLooseRefs(final TreeMap<String, Ref> avail) + throws TransportException { + try { + for (final String n : s3.list(bucket, resolveKey(ROOT_DIR + + "refs"))) + readRef(avail, "refs/" + n); + } catch (IOException e) { + throw new TransportException(getURI(), "cannot list refs", e); + } + } + + private Ref readRef(final TreeMap<String, Ref> avail, final String rn) + throws TransportException { + final String s; + String ref = ROOT_DIR + rn; + try { + final BufferedReader br = openReader(ref); + try { + s = br.readLine(); + } finally { + br.close(); + } + } catch (FileNotFoundException noRef) { + return null; + } catch (IOException err) { + throw new TransportException(getURI(), "read " + ref, err); + } + + if (s == null) + throw new TransportException(getURI(), "Empty ref: " + rn); + + if (s.startsWith("ref: ")) { + final String target = s.substring("ref: ".length()); + Ref r = avail.get(target); + if (r == null) + r = readRef(avail, target); + if (r == null) + return null; + r = new Ref(r.getStorage(), rn, r.getObjectId(), r + .getPeeledObjectId(), r.isPeeled()); + avail.put(r.getName(), r); + return r; + } + + if (ObjectId.isId(s)) { + final Ref r = new Ref(loose(avail.get(rn)), rn, ObjectId + .fromString(s)); + avail.put(r.getName(), r); + return r; + } + + throw new TransportException(getURI(), "Bad ref: " + rn + ": " + s); + } + + private Storage loose(final Ref r) { + if (r != null && r.getStorage() == Storage.PACKED) + return Storage.LOOSE_PACKED; + return Storage.LOOSE; + } + + @Override + void close() { + // We do not maintain persistent connections. + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundle.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundle.java new file mode 100644 index 0000000000..05be0bbdf7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundle.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +/** + * Marker interface for transports that supports fetching from a git bundle + * (sneaker-net object transport). + * <p> + * Push support for a bundle is complex, as one does not have a peer to + * communicate with to decide what the peer already knows. So push is not + * supported by the bundle transport. + */ +public interface TransportBundle extends PackTransport { + /** + * Bundle signature + */ + public static final String V2_BUNDLE_SIGNATURE = "# v2 git bundle"; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleFile.java new file mode 100644 index 0000000000..17e3bdd229 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleFile.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; + +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.FS; + +class TransportBundleFile extends Transport implements TransportBundle { + static boolean canHandle(final URIish uri) { + if (uri.getHost() != null || uri.getPort() > 0 || uri.getUser() != null + || uri.getPass() != null || uri.getPath() == null) + return false; + + if ("file".equals(uri.getScheme()) || uri.getScheme() == null) { + final File f = FS.resolve(new File("."), uri.getPath()); + return f.isFile() || f.getName().endsWith(".bundle"); + } + + return false; + } + + private final File bundle; + + TransportBundleFile(final Repository local, final URIish uri) { + super(local, uri); + bundle = FS.resolve(new File("."), uri.getPath()).getAbsoluteFile(); + } + + @Override + public FetchConnection openFetch() throws NotSupportedException, + TransportException { + final InputStream src; + try { + src = new FileInputStream(bundle); + } catch (FileNotFoundException err) { + throw new TransportException(uri, "not found"); + } + return new BundleFetchConnection(this, src); + } + + @Override + public PushConnection openPush() throws NotSupportedException { + throw new NotSupportedException( + "Push is not supported for bundle transport"); + } + + @Override + public void close() { + // Resources must be established per-connection. + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleStream.java new file mode 100644 index 0000000000..e5188bb236 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleStream.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; +import java.io.InputStream; + +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Repository; + +/** + * Single shot fetch from a streamed Git bundle. + * <p> + * The bundle is read from an unbuffered input stream, which limits the + * transport to opening at most one FetchConnection before needing to recreate + * the transport instance. + */ +public class TransportBundleStream extends Transport implements TransportBundle { + private InputStream src; + + /** + * Create a new transport to fetch objects from a streamed bundle. + * <p> + * The stream can be unbuffered (buffering is automatically provided + * internally to smooth out short reads) and unpositionable (the stream is + * read from only once, sequentially). + * <p> + * When the FetchConnection or the this instance is closed the supplied + * input stream is also automatically closed. This frees callers from + * needing to keep track of the supplied stream. + * + * @param db + * repository the fetched objects will be loaded into. + * @param uri + * symbolic name of the source of the stream. The URI can + * reference a non-existent resource. It is used only for + * exception reporting. + * @param in + * the stream to read the bundle from. + */ + public TransportBundleStream(final Repository db, final URIish uri, + final InputStream in) { + super(db, uri); + src = in; + } + + @Override + public FetchConnection openFetch() throws TransportException { + if (src == null) + throw new TransportException(uri, "Only one fetch supported"); + try { + return new BundleFetchConnection(this, src); + } finally { + src = null; + } + } + + @Override + public PushConnection openPush() throws NotSupportedException { + throw new NotSupportedException( + "Push is not supported for bundle transport"); + } + + @Override + public void close() { + if (src != null) { + try { + src.close(); + } catch (IOException err) { + // Ignore a close error. + } finally { + src = null; + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java new file mode 100644 index 0000000000..a127ff50ab --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Repository; + +/** + * Transport through a git-daemon waiting for anonymous TCP connections. + * <p> + * This transport supports the <code>git://</code> protocol, usually run on + * the IANA registered port 9418. It is a popular means for distributing open + * source projects, as there are no authentication or authorization overheads. + */ +class TransportGitAnon extends TcpTransport implements PackTransport { + static final int GIT_PORT = Daemon.DEFAULT_PORT; + + static boolean canHandle(final URIish uri) { + return "git".equals(uri.getScheme()); + } + + TransportGitAnon(final Repository local, final URIish uri) { + super(local, uri); + } + + @Override + public FetchConnection openFetch() throws TransportException { + return new TcpFetchConnection(); + } + + @Override + public PushConnection openPush() throws TransportException { + return new TcpPushConnection(); + } + + @Override + public void close() { + // Resources must be established per-connection. + } + + Socket openConnection() throws TransportException { + final int tms = getTimeout() > 0 ? getTimeout() * 1000 : 0; + final int port = uri.getPort() > 0 ? uri.getPort() : GIT_PORT; + final Socket s = new Socket(); + try { + final InetAddress host = InetAddress.getByName(uri.getHost()); + s.bind(null); + s.connect(new InetSocketAddress(host, port), tms); + } catch (IOException c) { + try { + s.close(); + } catch (IOException closeErr) { + // ignore a failure during close, we're already failing + } + if (c instanceof UnknownHostException) + throw new TransportException(uri, "unknown host"); + if (c instanceof ConnectException) + throw new TransportException(uri, c.getMessage()); + throw new TransportException(uri, c.getMessage(), c); + } + return s; + } + + void service(final String name, final PacketLineOut pckOut) + throws IOException { + final StringBuilder cmd = new StringBuilder(); + cmd.append(name); + cmd.append(' '); + cmd.append(uri.getPath()); + cmd.append('\0'); + cmd.append("host="); + cmd.append(uri.getHost()); + if (uri.getPort() > 0 && uri.getPort() != GIT_PORT) { + cmd.append(":"); + cmd.append(uri.getPort()); + } + cmd.append('\0'); + pckOut.writeString(cmd.toString()); + pckOut.flush(); + } + + class TcpFetchConnection extends BasePackFetchConnection { + private Socket sock; + + TcpFetchConnection() throws TransportException { + super(TransportGitAnon.this); + sock = openConnection(); + try { + init(sock.getInputStream(), sock.getOutputStream()); + service("git-upload-pack", pckOut); + } catch (IOException err) { + close(); + throw new TransportException(uri, + "remote hung up unexpectedly", err); + } + readAdvertisedRefs(); + } + + @Override + public void close() { + super.close(); + + if (sock != null) { + try { + sock.close(); + } catch (IOException err) { + // Ignore errors during close. + } finally { + sock = null; + } + } + } + } + + class TcpPushConnection extends BasePackPushConnection { + private Socket sock; + + TcpPushConnection() throws TransportException { + super(TransportGitAnon.this); + sock = openConnection(); + try { + init(sock.getInputStream(), sock.getOutputStream()); + service("git-receive-pack", pckOut); + } catch (IOException err) { + close(); + throw new TransportException(uri, + "remote hung up unexpectedly", err); + } + readAdvertisedRefs(); + } + + @Override + public void close() { + super.close(); + + if (sock != null) { + try { + sock.close(); + } catch (IOException err) { + // Ignore errors during close. + } finally { + sock = null; + } + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java new file mode 100644 index 0000000000..55636f8dcc --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; + +import org.eclipse.jgit.errors.NoRemoteRepositoryException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.QuotedString; + +import com.jcraft.jsch.ChannelExec; +import com.jcraft.jsch.JSchException; + +/** + * Transport through an SSH tunnel. + * <p> + * The SSH transport requires the remote side to have Git installed, as the + * transport logs into the remote system and executes a Git helper program on + * the remote side to read (or write) the remote repository's files. + * <p> + * This transport does not support direct SCP style of copying files, as it + * assumes there are Git specific smarts on the remote side to perform object + * enumeration, save file modification and hook execution. + */ +public class TransportGitSsh extends SshTransport implements PackTransport { + static boolean canHandle(final URIish uri) { + if (!uri.isRemote()) + return false; + final String scheme = uri.getScheme(); + if ("ssh".equals(scheme)) + return true; + if ("ssh+git".equals(scheme)) + return true; + if ("git+ssh".equals(scheme)) + return true; + if (scheme == null && uri.getHost() != null && uri.getPath() != null) + return true; + return false; + } + + OutputStream errStream; + + TransportGitSsh(final Repository local, final URIish uri) { + super(local, uri); + } + + @Override + public FetchConnection openFetch() throws TransportException { + return new SshFetchConnection(); + } + + @Override + public PushConnection openPush() throws TransportException { + return new SshPushConnection(); + } + + private static void sqMinimal(final StringBuilder cmd, final String val) { + if (val.matches("^[a-zA-Z0-9._/-]*$")) { + // If the string matches only generally safe characters + // that the shell is not going to evaluate specially we + // should leave the string unquoted. Not all systems + // actually run a shell and over-quoting confuses them + // when it comes to the command name. + // + cmd.append(val); + } else { + sq(cmd, val); + } + } + + private static void sqAlways(final StringBuilder cmd, final String val) { + sq(cmd, val); + } + + private static void sq(final StringBuilder cmd, final String val) { + if (val.length() > 0) + cmd.append(QuotedString.BOURNE.quote(val)); + } + + + ChannelExec exec(final String exe) throws TransportException { + initSession(); + + final int tms = getTimeout() > 0 ? getTimeout() * 1000 : 0; + try { + final ChannelExec channel = (ChannelExec) sock.openChannel("exec"); + String path = uri.getPath(); + if (uri.getScheme() != null && uri.getPath().startsWith("/~")) + path = (uri.getPath().substring(1)); + + final StringBuilder cmd = new StringBuilder(); + final int gitspace = exe.indexOf("git "); + if (gitspace >= 0) { + sqMinimal(cmd, exe.substring(0, gitspace + 3)); + cmd.append(' '); + sqMinimal(cmd, exe.substring(gitspace + 4)); + } else + sqMinimal(cmd, exe); + cmd.append(' '); + sqAlways(cmd, path); + channel.setCommand(cmd.toString()); + errStream = createErrorStream(); + channel.setErrStream(errStream, true); + channel.connect(tms); + return channel; + } catch (JSchException je) { + throw new TransportException(uri, je.getMessage(), je); + } + } + + /** + * @return the error stream for the channel, the stream is used to detect + * specific error reasons for exceptions. + */ + private static OutputStream createErrorStream() { + return new OutputStream() { + private StringBuilder all = new StringBuilder(); + + private StringBuilder sb = new StringBuilder(); + + public String toString() { + String r = all.toString(); + while (r.endsWith("\n")) + r = r.substring(0, r.length() - 1); + return r; + } + + @Override + public void write(final int b) throws IOException { + if (b == '\r') { + return; + } + + sb.append((char) b); + + if (b == '\n') { + all.append(sb); + sb.setLength(0); + } + } + }; + } + + NoRemoteRepositoryException cleanNotFound(NoRemoteRepositoryException nf) { + String why = errStream.toString(); + if (why == null || why.length() == 0) + return nf; + + String path = uri.getPath(); + if (uri.getScheme() != null && uri.getPath().startsWith("/~")) + path = uri.getPath().substring(1); + + final StringBuilder pfx = new StringBuilder(); + pfx.append("fatal: "); + sqAlways(pfx, path); + pfx.append(": "); + if (why.startsWith(pfx.toString())) + why = why.substring(pfx.length()); + + return new NoRemoteRepositoryException(uri, why); + } + + // JSch won't let us interrupt writes when we use our InterruptTimer to + // break out of a long-running write operation. To work around that we + // spawn a background thread to shuttle data through a pipe, as we can + // issue an interrupted write out of that. Its slower, so we only use + // this route if there is a timeout. + // + private OutputStream outputStream(ChannelExec channel) throws IOException { + final OutputStream out = channel.getOutputStream(); + if (getTimeout() <= 0) + return out; + final PipedInputStream pipeIn = new PipedInputStream(); + final CopyThread copyThread = new CopyThread(pipeIn, out); + final PipedOutputStream pipeOut = new PipedOutputStream(pipeIn) { + @Override + public void flush() throws IOException { + super.flush(); + copyThread.flush(); + } + + @Override + public void close() throws IOException { + super.close(); + try { + copyThread.join(getTimeout() * 1000); + } catch (InterruptedException e) { + // Just wake early, the thread will terminate anyway. + } + } + }; + copyThread.start(); + return pipeOut; + } + + private static class CopyThread extends Thread { + private final InputStream src; + + private final OutputStream dst; + + private volatile boolean doFlush; + + CopyThread(final InputStream i, final OutputStream o) { + setName(Thread.currentThread().getName() + "-Output"); + src = i; + dst = o; + } + + void flush() { + if (!doFlush) { + doFlush = true; + interrupt(); + } + } + + @Override + public void run() { + try { + final byte[] buf = new byte[1024]; + for (;;) { + try { + if (doFlush) { + doFlush = false; + dst.flush(); + } + + final int n; + try { + n = src.read(buf); + } catch (InterruptedIOException wakey) { + continue; + } + if (n < 0) + break; + dst.write(buf, 0, n); + } catch (IOException e) { + break; + } + } + } finally { + try { + src.close(); + } catch (IOException e) { + // Ignore IO errors on close + } + try { + dst.close(); + } catch (IOException e) { + // Ignore IO errors on close + } + } + } + } + + class SshFetchConnection extends BasePackFetchConnection { + private ChannelExec channel; + + SshFetchConnection() throws TransportException { + super(TransportGitSsh.this); + try { + channel = exec(getOptionUploadPack()); + + if (channel.isConnected()) + init(channel.getInputStream(), outputStream(channel)); + else + throw new TransportException(uri, errStream.toString()); + + } catch (TransportException err) { + close(); + throw err; + } catch (IOException err) { + close(); + throw new TransportException(uri, + "remote hung up unexpectedly", err); + } + + try { + readAdvertisedRefs(); + } catch (NoRemoteRepositoryException notFound) { + throw cleanNotFound(notFound); + } + } + + @Override + public void close() { + super.close(); + + if (channel != null) { + try { + if (channel.isConnected()) + channel.disconnect(); + } finally { + channel = null; + } + } + } + } + + class SshPushConnection extends BasePackPushConnection { + private ChannelExec channel; + + SshPushConnection() throws TransportException { + super(TransportGitSsh.this); + try { + channel = exec(getOptionReceivePack()); + + if (channel.isConnected()) + init(channel.getInputStream(), outputStream(channel)); + else + throw new TransportException(uri, errStream.toString()); + + } catch (TransportException err) { + close(); + throw err; + } catch (IOException err) { + close(); + throw new TransportException(uri, + "remote hung up unexpectedly", err); + } + + try { + readAdvertisedRefs(); + } catch (NoRemoteRepositoryException notFound) { + throw cleanNotFound(notFound); + } + } + + @Override + public void close() { + super.close(); + + if (channel != null) { + try { + if (channel.isConnected()) + channel.disconnect(); + } finally { + channel = null; + } + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java new file mode 100644 index 0000000000..65686b9d42 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.TreeMap; + +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.PackProtocolException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.HttpSupport; + +/** + * Transport over the non-Git aware HTTP and FTP protocol. + * <p> + * The HTTP transport does not require any specialized Git support on the remote + * (server side) repository. Object files are retrieved directly through + * standard HTTP GET requests, making it easy to serve a Git repository through + * a standard web host provider that does not offer specific support for Git. + * + * @see WalkFetchConnection + */ +public class TransportHttp extends HttpTransport implements WalkTransport { + static boolean canHandle(final URIish uri) { + if (!uri.isRemote()) + return false; + final String s = uri.getScheme(); + return "http".equals(s) || "https".equals(s) || "ftp".equals(s); + } + + private final URL baseUrl; + + private final URL objectsUrl; + + private final ProxySelector proxySelector; + + TransportHttp(final Repository local, final URIish uri) + throws NotSupportedException { + super(local, uri); + try { + String uriString = uri.toString(); + if (!uriString.endsWith("/")) + uriString += "/"; + baseUrl = new URL(uriString); + objectsUrl = new URL(baseUrl, "objects/"); + } catch (MalformedURLException e) { + throw new NotSupportedException("Invalid URL " + uri, e); + } + proxySelector = ProxySelector.getDefault(); + } + + @Override + public FetchConnection openFetch() throws TransportException { + final HttpObjectDB c = new HttpObjectDB(objectsUrl); + final WalkFetchConnection r = new WalkFetchConnection(this, c); + r.available(c.readAdvertisedRefs()); + return r; + } + + @Override + public PushConnection openPush() throws NotSupportedException, + TransportException { + final String s = getURI().getScheme(); + throw new NotSupportedException("Push not supported over " + s + "."); + } + + @Override + public void close() { + // No explicit connections are maintained. + } + + class HttpObjectDB extends WalkRemoteObjectDatabase { + private final URL objectsUrl; + + HttpObjectDB(final URL b) { + objectsUrl = b; + } + + @Override + URIish getURI() { + return new URIish(objectsUrl); + } + + @Override + Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException { + try { + return readAlternates(INFO_HTTP_ALTERNATES); + } catch (FileNotFoundException err) { + // Fall through. + } + + try { + return readAlternates(INFO_ALTERNATES); + } catch (FileNotFoundException err) { + // Fall through. + } + + return null; + } + + @Override + WalkRemoteObjectDatabase openAlternate(final String location) + throws IOException { + return new HttpObjectDB(new URL(objectsUrl, location)); + } + + @Override + Collection<String> getPackNames() throws IOException { + final Collection<String> packs = new ArrayList<String>(); + try { + final BufferedReader br = openReader(INFO_PACKS); + try { + for (;;) { + final String s = br.readLine(); + if (s == null || s.length() == 0) + break; + if (!s.startsWith("P pack-") || !s.endsWith(".pack")) + throw invalidAdvertisement(s); + packs.add(s.substring(2)); + } + return packs; + } finally { + br.close(); + } + } catch (FileNotFoundException err) { + return packs; + } + } + + @Override + FileStream open(final String path) throws IOException { + final URL base = objectsUrl; + final URL u = new URL(base, path); + final Proxy proxy = HttpSupport.proxyFor(proxySelector, u); + final HttpURLConnection c; + + c = (HttpURLConnection) u.openConnection(proxy); + switch (HttpSupport.response(c)) { + case HttpURLConnection.HTTP_OK: + final InputStream in = c.getInputStream(); + final int len = c.getContentLength(); + return new FileStream(in, len); + case HttpURLConnection.HTTP_NOT_FOUND: + throw new FileNotFoundException(u.toString()); + default: + throw new IOException(u.toString() + ": " + + HttpSupport.response(c) + " " + + c.getResponseMessage()); + } + } + + Map<String, Ref> readAdvertisedRefs() throws TransportException { + try { + final BufferedReader br = openReader(INFO_REFS); + try { + return readAdvertisedImpl(br); + } finally { + br.close(); + } + } catch (IOException err) { + try { + throw new TransportException(new URL(objectsUrl, INFO_REFS) + + ": cannot read available refs", err); + } catch (MalformedURLException mue) { + throw new TransportException(objectsUrl + INFO_REFS + + ": cannot read available refs", err); + } + } + } + + private Map<String, Ref> readAdvertisedImpl(final BufferedReader br) + throws IOException, PackProtocolException { + final TreeMap<String, Ref> avail = new TreeMap<String, Ref>(); + for (;;) { + String line = br.readLine(); + if (line == null) + break; + + final int tab = line.indexOf('\t'); + if (tab < 0) + throw invalidAdvertisement(line); + + String name; + final ObjectId id; + + name = line.substring(tab + 1); + id = ObjectId.fromString(line.substring(0, tab)); + if (name.endsWith("^{}")) { + name = name.substring(0, name.length() - 3); + final Ref prior = avail.get(name); + if (prior == null) + throw outOfOrderAdvertisement(name); + + if (prior.getPeeledObjectId() != null) + throw duplicateAdvertisement(name + "^{}"); + + avail.put(name, new Ref(Ref.Storage.NETWORK, name, prior + .getObjectId(), id, true)); + } else { + final Ref prior = avail.put(name, new Ref( + Ref.Storage.NETWORK, name, id)); + if (prior != null) + throw duplicateAdvertisement(name); + } + } + return avail; + } + + private PackProtocolException outOfOrderAdvertisement(final String n) { + return new PackProtocolException("advertisement of " + n + + "^{} came before " + n); + } + + private PackProtocolException invalidAdvertisement(final String n) { + return new PackProtocolException("invalid advertisement of " + n); + } + + private PackProtocolException duplicateAdvertisement(final String n) { + return new PackProtocolException("duplicate advertisements of " + n); + } + + @Override + void close() { + // We do not maintain persistent connections. + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java new file mode 100644 index 0000000000..8bb22275b5 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; + +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.FS; + +/** + * Transport to access a local directory as though it were a remote peer. + * <p> + * This transport is suitable for use on the local system, where the caller has + * direct read or write access to the "remote" repository. + * <p> + * By default this transport works by spawning a helper thread within the same + * JVM, and processes the data transfer using a shared memory buffer between the + * calling thread and the helper thread. This is a pure-Java implementation + * which does not require forking an external process. + * <p> + * However, during {@link #openFetch()}, if the Transport has configured + * {@link Transport#getOptionUploadPack()} to be anything other than + * <code>"git-upload-pack"</code> or <code>"git upload-pack"</code>, this + * implementation will fork and execute the external process, using an operating + * system pipe to transfer data. + * <p> + * Similarly, during {@link #openPush()}, if the Transport has configured + * {@link Transport#getOptionReceivePack()} to be anything other than + * <code>"git-receive-pack"</code> or <code>"git receive-pack"</code>, this + * implementation will fork and execute the external process, using an operating + * system pipe to transfer data. + */ +class TransportLocal extends Transport implements PackTransport { + private static final String PWD = "."; + + static boolean canHandle(final URIish uri) { + if (uri.getHost() != null || uri.getPort() > 0 || uri.getUser() != null + || uri.getPass() != null || uri.getPath() == null) + return false; + + if ("file".equals(uri.getScheme()) || uri.getScheme() == null) + return FS.resolve(new File(PWD), uri.getPath()).isDirectory(); + return false; + } + + private final File remoteGitDir; + + TransportLocal(final Repository local, final URIish uri) { + super(local, uri); + + File d = FS.resolve(new File(PWD), uri.getPath()).getAbsoluteFile(); + if (new File(d, ".git").isDirectory()) + d = new File(d, ".git"); + remoteGitDir = d; + } + + @Override + public FetchConnection openFetch() throws TransportException { + final String up = getOptionUploadPack(); + if ("git-upload-pack".equals(up) || "git upload-pack".equals(up)) + return new InternalLocalFetchConnection(); + return new ForkLocalFetchConnection(); + } + + @Override + public PushConnection openPush() throws NotSupportedException, + TransportException { + final String rp = getOptionReceivePack(); + if ("git-receive-pack".equals(rp) || "git receive-pack".equals(rp)) + return new InternalLocalPushConnection(); + return new ForkLocalPushConnection(); + } + + @Override + public void close() { + // Resources must be established per-connection. + } + + protected Process startProcessWithErrStream(final String cmd) + throws TransportException { + try { + final String[] args; + final Process proc; + + if (cmd.startsWith("git-")) { + args = new String[] { "git", cmd.substring(4), PWD }; + } else { + final int gitspace = cmd.indexOf("git "); + if (gitspace >= 0) { + final String git = cmd.substring(0, gitspace + 3); + final String subcmd = cmd.substring(gitspace + 4); + args = new String[] { git, subcmd, PWD }; + } else { + args = new String[] { cmd, PWD }; + } + } + + proc = Runtime.getRuntime().exec(args, null, remoteGitDir); + new StreamRewritingThread(cmd, proc.getErrorStream()).start(); + return proc; + } catch (IOException err) { + throw new TransportException(uri, err.getMessage(), err); + } + } + + class InternalLocalFetchConnection extends BasePackFetchConnection { + private Thread worker; + + InternalLocalFetchConnection() throws TransportException { + super(TransportLocal.this); + + final Repository dst; + try { + dst = new Repository(remoteGitDir); + } catch (IOException err) { + throw new TransportException(uri, "not a git directory"); + } + + final PipedInputStream in_r; + final PipedOutputStream in_w; + + final PipedInputStream out_r; + final PipedOutputStream out_w; + try { + in_r = new PipedInputStream(); + in_w = new PipedOutputStream(in_r); + + out_r = new PipedInputStream() { + // The client (BasePackFetchConnection) can write + // a huge burst before it reads again. We need to + // force the buffer to be big enough, otherwise it + // will deadlock both threads. + { + buffer = new byte[MIN_CLIENT_BUFFER]; + } + }; + out_w = new PipedOutputStream(out_r); + } catch (IOException err) { + dst.close(); + throw new TransportException(uri, "cannot connect pipes", err); + } + + worker = new Thread("JGit-Upload-Pack") { + public void run() { + try { + final UploadPack rp = new UploadPack(dst); + rp.upload(out_r, in_w, null); + } catch (IOException err) { + // Client side of the pipes should report the problem. + err.printStackTrace(); + } catch (RuntimeException err) { + // Clients side will notice we went away, and report. + err.printStackTrace(); + } finally { + try { + out_r.close(); + } catch (IOException e2) { + // Ignore close failure, we probably crashed above. + } + + try { + in_w.close(); + } catch (IOException e2) { + // Ignore close failure, we probably crashed above. + } + + dst.close(); + } + } + }; + worker.start(); + + init(in_r, out_w); + readAdvertisedRefs(); + } + + @Override + public void close() { + super.close(); + + if (worker != null) { + try { + worker.join(); + } catch (InterruptedException ie) { + // Stop waiting and return anyway. + } finally { + worker = null; + } + } + } + } + + class ForkLocalFetchConnection extends BasePackFetchConnection { + private Process uploadPack; + + ForkLocalFetchConnection() throws TransportException { + super(TransportLocal.this); + uploadPack = startProcessWithErrStream(getOptionUploadPack()); + final InputStream upIn = uploadPack.getInputStream(); + final OutputStream upOut = uploadPack.getOutputStream(); + init(upIn, upOut); + readAdvertisedRefs(); + } + + @Override + public void close() { + super.close(); + + if (uploadPack != null) { + try { + uploadPack.waitFor(); + } catch (InterruptedException ie) { + // Stop waiting and return anyway. + } finally { + uploadPack = null; + } + } + } + } + + class InternalLocalPushConnection extends BasePackPushConnection { + private Thread worker; + + InternalLocalPushConnection() throws TransportException { + super(TransportLocal.this); + + final Repository dst; + try { + dst = new Repository(remoteGitDir); + } catch (IOException err) { + throw new TransportException(uri, "not a git directory"); + } + + final PipedInputStream in_r; + final PipedOutputStream in_w; + + final PipedInputStream out_r; + final PipedOutputStream out_w; + try { + in_r = new PipedInputStream(); + in_w = new PipedOutputStream(in_r); + + out_r = new PipedInputStream(); + out_w = new PipedOutputStream(out_r); + } catch (IOException err) { + dst.close(); + throw new TransportException(uri, "cannot connect pipes", err); + } + + worker = new Thread("JGit-Receive-Pack") { + public void run() { + try { + final ReceivePack rp = new ReceivePack(dst); + rp.receive(out_r, in_w, System.err); + } catch (IOException err) { + // Client side of the pipes should report the problem. + } catch (RuntimeException err) { + // Clients side will notice we went away, and report. + } finally { + try { + out_r.close(); + } catch (IOException e2) { + // Ignore close failure, we probably crashed above. + } + + try { + in_w.close(); + } catch (IOException e2) { + // Ignore close failure, we probably crashed above. + } + + dst.close(); + } + } + }; + worker.start(); + + init(in_r, out_w); + readAdvertisedRefs(); + } + + @Override + public void close() { + super.close(); + + if (worker != null) { + try { + worker.join(); + } catch (InterruptedException ie) { + // Stop waiting and return anyway. + } finally { + worker = null; + } + } + } + } + + class ForkLocalPushConnection extends BasePackPushConnection { + private Process receivePack; + + ForkLocalPushConnection() throws TransportException { + super(TransportLocal.this); + receivePack = startProcessWithErrStream(getOptionReceivePack()); + final InputStream rpIn = receivePack.getInputStream(); + final OutputStream rpOut = receivePack.getOutputStream(); + init(rpIn, rpOut); + readAdvertisedRefs(); + } + + @Override + public void close() { + super.close(); + + if (receivePack != null) { + try { + receivePack.waitFor(); + } catch (InterruptedException ie) { + // Stop waiting and return anyway. + } finally { + receivePack = null; + } + } + } + } + + static class StreamRewritingThread extends Thread { + private final InputStream in; + + StreamRewritingThread(final String cmd, final InputStream in) { + super("JGit " + cmd + " Errors"); + this.in = in; + } + + public void run() { + final byte[] tmp = new byte[512]; + try { + for (;;) { + final int n = in.read(tmp); + if (n < 0) + break; + System.err.write(tmp, 0, n); + System.err.flush(); + } + } catch (IOException err) { + // Ignore errors reading errors. + } finally { + try { + in.close(); + } catch (IOException err2) { + // Ignore errors closing the pipe. + } + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java new file mode 100644 index 0000000000..8243ddabb2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Ref.Storage; + +import com.jcraft.jsch.Channel; +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.SftpATTRS; +import com.jcraft.jsch.SftpException; + +/** + * Transport over the non-Git aware SFTP (SSH based FTP) protocol. + * <p> + * The SFTP transport does not require any specialized Git support on the remote + * (server side) repository. Object files are retrieved directly through secure + * shell's FTP protocol, making it possible to copy objects from a remote + * repository that is available over SSH, but whose remote host does not have + * Git installed. + * <p> + * Unlike the HTTP variant (see {@link TransportHttp}) we rely upon being able + * to list files in directories, as the SFTP protocol supports this function. By + * listing files through SFTP we can avoid needing to have current + * <code>objects/info/packs</code> or <code>info/refs</code> files on the + * remote repository and access the data directly, much as Git itself would. + * <p> + * Concurrent pushing over this transport is not supported. Multiple concurrent + * push operations may cause confusion in the repository state. + * + * @see WalkFetchConnection + */ +public class TransportSftp extends SshTransport implements WalkTransport { + static boolean canHandle(final URIish uri) { + return uri.isRemote() && "sftp".equals(uri.getScheme()); + } + + TransportSftp(final Repository local, final URIish uri) { + super(local, uri); + } + + @Override + public FetchConnection openFetch() throws TransportException { + final SftpObjectDB c = new SftpObjectDB(uri.getPath()); + final WalkFetchConnection r = new WalkFetchConnection(this, c); + r.available(c.readAdvertisedRefs()); + return r; + } + + @Override + public PushConnection openPush() throws TransportException { + final SftpObjectDB c = new SftpObjectDB(uri.getPath()); + final WalkPushConnection r = new WalkPushConnection(this, c); + r.available(c.readAdvertisedRefs()); + return r; + } + + ChannelSftp newSftp() throws TransportException { + initSession(); + + final int tms = getTimeout() > 0 ? getTimeout() * 1000 : 0; + try { + final Channel channel = sock.openChannel("sftp"); + channel.connect(tms); + return (ChannelSftp) channel; + } catch (JSchException je) { + throw new TransportException(uri, je.getMessage(), je); + } + } + + class SftpObjectDB extends WalkRemoteObjectDatabase { + private final String objectsPath; + + private ChannelSftp ftp; + + SftpObjectDB(String path) throws TransportException { + if (path.startsWith("/~")) + path = path.substring(1); + if (path.startsWith("~/")) + path = path.substring(2); + try { + ftp = newSftp(); + ftp.cd(path); + ftp.cd("objects"); + objectsPath = ftp.pwd(); + } catch (TransportException err) { + close(); + throw err; + } catch (SftpException je) { + throw new TransportException("Can't enter " + path + "/objects" + + ": " + je.getMessage(), je); + } + } + + SftpObjectDB(final SftpObjectDB parent, final String p) + throws TransportException { + try { + ftp = newSftp(); + ftp.cd(parent.objectsPath); + ftp.cd(p); + objectsPath = ftp.pwd(); + } catch (TransportException err) { + close(); + throw err; + } catch (SftpException je) { + throw new TransportException("Can't enter " + p + " from " + + parent.objectsPath + ": " + je.getMessage(), je); + } + } + + @Override + URIish getURI() { + return uri.setPath(objectsPath); + } + + @Override + Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException { + try { + return readAlternates(INFO_ALTERNATES); + } catch (FileNotFoundException err) { + return null; + } + } + + @Override + WalkRemoteObjectDatabase openAlternate(final String location) + throws IOException { + return new SftpObjectDB(this, location); + } + + @Override + Collection<String> getPackNames() throws IOException { + final List<String> packs = new ArrayList<String>(); + try { + final Collection<ChannelSftp.LsEntry> list = ftp.ls("pack"); + final HashMap<String, ChannelSftp.LsEntry> files; + final HashMap<String, Integer> mtimes; + + files = new HashMap<String, ChannelSftp.LsEntry>(); + mtimes = new HashMap<String, Integer>(); + + for (final ChannelSftp.LsEntry ent : list) + files.put(ent.getFilename(), ent); + for (final ChannelSftp.LsEntry ent : list) { + final String n = ent.getFilename(); + if (!n.startsWith("pack-") || !n.endsWith(".pack")) + continue; + + final String in = n.substring(0, n.length() - 5) + ".idx"; + if (!files.containsKey(in)) + continue; + + mtimes.put(n, ent.getAttrs().getMTime()); + packs.add(n); + } + + Collections.sort(packs, new Comparator<String>() { + public int compare(final String o1, final String o2) { + return mtimes.get(o2) - mtimes.get(o1); + } + }); + } catch (SftpException je) { + throw new TransportException("Can't ls " + objectsPath + + "/pack: " + je.getMessage(), je); + } + return packs; + } + + @Override + FileStream open(final String path) throws IOException { + try { + final SftpATTRS a = ftp.lstat(path); + return new FileStream(ftp.get(path), a.getSize()); + } catch (SftpException je) { + if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) + throw new FileNotFoundException(path); + throw new TransportException("Can't get " + objectsPath + "/" + + path + ": " + je.getMessage(), je); + } + } + + @Override + void deleteFile(final String path) throws IOException { + try { + ftp.rm(path); + } catch (SftpException je) { + if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) + return; + throw new TransportException("Can't delete " + objectsPath + + "/" + path + ": " + je.getMessage(), je); + } + + // Prune any now empty directories. + // + String dir = path; + int s = dir.lastIndexOf('/'); + while (s > 0) { + try { + dir = dir.substring(0, s); + ftp.rmdir(dir); + s = dir.lastIndexOf('/'); + } catch (SftpException je) { + // If we cannot delete it, leave it alone. It may have + // entries still in it, or maybe we lack write access on + // the parent. Either way it isn't a fatal error. + // + break; + } + } + } + + @Override + OutputStream writeFile(final String path, + final ProgressMonitor monitor, final String monitorTask) + throws IOException { + try { + return ftp.put(path); + } catch (SftpException je) { + if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { + mkdir_p(path); + try { + return ftp.put(path); + } catch (SftpException je2) { + je = je2; + } + } + + throw new TransportException("Can't write " + objectsPath + "/" + + path + ": " + je.getMessage(), je); + } + } + + @Override + void writeFile(final String path, final byte[] data) throws IOException { + final String lock = path + ".lock"; + try { + super.writeFile(lock, data); + try { + ftp.rename(lock, path); + } catch (SftpException je) { + throw new TransportException("Can't write " + objectsPath + + "/" + path + ": " + je.getMessage(), je); + } + } catch (IOException err) { + try { + ftp.rm(lock); + } catch (SftpException e) { + // Ignore deletion failure, we are already + // failing anyway. + } + throw err; + } + } + + private void mkdir_p(String path) throws IOException { + final int s = path.lastIndexOf('/'); + if (s <= 0) + return; + + path = path.substring(0, s); + try { + ftp.mkdir(path); + } catch (SftpException je) { + if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { + mkdir_p(path); + try { + ftp.mkdir(path); + return; + } catch (SftpException je2) { + je = je2; + } + } + + throw new TransportException("Can't mkdir " + objectsPath + "/" + + path + ": " + je.getMessage(), je); + } + } + + Map<String, Ref> readAdvertisedRefs() throws TransportException { + final TreeMap<String, Ref> avail = new TreeMap<String, Ref>(); + readPackedRefs(avail); + readRef(avail, ROOT_DIR + Constants.HEAD, Constants.HEAD); + readLooseRefs(avail, ROOT_DIR + "refs", "refs/"); + return avail; + } + + private void readLooseRefs(final TreeMap<String, Ref> avail, + final String dir, final String prefix) + throws TransportException { + final Collection<ChannelSftp.LsEntry> list; + try { + list = ftp.ls(dir); + } catch (SftpException je) { + throw new TransportException("Can't ls " + objectsPath + "/" + + dir + ": " + je.getMessage(), je); + } + + for (final ChannelSftp.LsEntry ent : list) { + final String n = ent.getFilename(); + if (".".equals(n) || "..".equals(n)) + continue; + + final String nPath = dir + "/" + n; + if (ent.getAttrs().isDir()) + readLooseRefs(avail, nPath, prefix + n + "/"); + else + readRef(avail, nPath, prefix + n); + } + } + + private Ref readRef(final TreeMap<String, Ref> avail, + final String path, final String name) throws TransportException { + final String line; + try { + final BufferedReader br = openReader(path); + try { + line = br.readLine(); + } finally { + br.close(); + } + } catch (FileNotFoundException noRef) { + return null; + } catch (IOException err) { + throw new TransportException("Cannot read " + objectsPath + "/" + + path + ": " + err.getMessage(), err); + } + + if (line == null) + throw new TransportException("Empty ref: " + name); + + if (line.startsWith("ref: ")) { + final String p = line.substring("ref: ".length()); + Ref r = readRef(avail, ROOT_DIR + p, p); + if (r == null) + r = avail.get(p); + if (r != null) { + r = new Ref(loose(r), name, r.getObjectId(), r + .getPeeledObjectId(), true); + avail.put(name, r); + } + return r; + } + + if (ObjectId.isId(line)) { + final Ref r = new Ref(loose(avail.get(name)), name, ObjectId + .fromString(line)); + avail.put(r.getName(), r); + return r; + } + + throw new TransportException("Bad ref: " + name + ": " + line); + } + + private Storage loose(final Ref r) { + if (r != null && r.getStorage() == Storage.PACKED) + return Storage.LOOSE_PACKED; + return Storage.LOOSE; + } + + @Override + void close() { + if (ftp != null) { + try { + if (ftp.isConnected()) + ftp.disconnect(); + } finally { + ftp = null; + } + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java new file mode 100644 index 0000000000..cfdf47c0c4 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.net.URISyntaxException; +import java.net.URL; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This URI like construct used for referencing Git archives over the net, as + * well as locally stored archives. The most important difference compared to + * RFC 2396 URI's is that no URI encoding/decoding ever takes place. A space or + * any special character is written as-is. + */ +public class URIish { + private static final Pattern FULL_URI = Pattern + .compile("^(?:([a-z][a-z0-9+-]+)://(?:([^/]+?)(?::([^/]+?))?@)?(?:([^/]+?))?(?::(\\d+))?)?((?:[A-Za-z]:)?/.+)$"); + + private static final Pattern SCP_URI = Pattern + .compile("^(?:([^@]+?)@)?([^:]+?):(.+)$"); + + private String scheme; + + private String path; + + private String user; + + private String pass; + + private int port = -1; + + private String host; + + /** + * Parse and construct an {@link URIish} from a string + * + * @param s + * @throws URISyntaxException + */ + public URIish(String s) throws URISyntaxException { + s = s.replace('\\', '/'); + Matcher matcher = FULL_URI.matcher(s); + if (matcher.matches()) { + scheme = matcher.group(1); + user = matcher.group(2); + pass = matcher.group(3); + host = matcher.group(4); + if (matcher.group(5) != null) + port = Integer.parseInt(matcher.group(5)); + path = matcher.group(6); + if (path.length() >= 3 + && path.charAt(0) == '/' + && path.charAt(2) == ':' + && (path.charAt(1) >= 'A' && path.charAt(1) <= 'Z' + || path.charAt(1) >= 'a' && path.charAt(1) <= 'z')) + path = path.substring(1); + } else { + matcher = SCP_URI.matcher(s); + if (matcher.matches()) { + user = matcher.group(1); + host = matcher.group(2); + path = matcher.group(3); + } else + throw new URISyntaxException(s, "Cannot parse Git URI-ish"); + } + } + + /** + * Construct a URIish from a standard URL. + * + * @param u + * the source URL to convert from. + */ + public URIish(final URL u) { + scheme = u.getProtocol(); + path = u.getPath(); + + final String ui = u.getUserInfo(); + if (ui != null) { + final int d = ui.indexOf(':'); + user = d < 0 ? ui : ui.substring(0, d); + pass = d < 0 ? null : ui.substring(d + 1); + } + + port = u.getPort(); + host = u.getHost(); + } + + /** Create an empty, non-configured URI. */ + public URIish() { + // Configure nothing. + } + + private URIish(final URIish u) { + this.scheme = u.scheme; + this.path = u.path; + this.user = u.user; + this.pass = u.pass; + this.port = u.port; + this.host = u.host; + } + + /** + * @return true if this URI references a repository on another system. + */ + public boolean isRemote() { + return getHost() != null; + } + + /** + * @return host name part or null + */ + public String getHost() { + return host; + } + + /** + * Return a new URI matching this one, but with a different host. + * + * @param n + * the new value for host. + * @return a new URI with the updated value. + */ + public URIish setHost(final String n) { + final URIish r = new URIish(this); + r.host = n; + return r; + } + + /** + * @return protocol name or null for local references + */ + public String getScheme() { + return scheme; + } + + /** + * Return a new URI matching this one, but with a different scheme. + * + * @param n + * the new value for scheme. + * @return a new URI with the updated value. + */ + public URIish setScheme(final String n) { + final URIish r = new URIish(this); + r.scheme = n; + return r; + } + + /** + * @return path name component + */ + public String getPath() { + return path; + } + + /** + * Return a new URI matching this one, but with a different path. + * + * @param n + * the new value for path. + * @return a new URI with the updated value. + */ + public URIish setPath(final String n) { + final URIish r = new URIish(this); + r.path = n; + return r; + } + + /** + * @return user name requested for transfer or null + */ + public String getUser() { + return user; + } + + /** + * Return a new URI matching this one, but with a different user. + * + * @param n + * the new value for user. + * @return a new URI with the updated value. + */ + public URIish setUser(final String n) { + final URIish r = new URIish(this); + r.user = n; + return r; + } + + /** + * @return password requested for transfer or null + */ + public String getPass() { + return pass; + } + + /** + * Return a new URI matching this one, but with a different password. + * + * @param n + * the new value for password. + * @return a new URI with the updated value. + */ + public URIish setPass(final String n) { + final URIish r = new URIish(this); + r.pass = n; + return r; + } + + /** + * @return port number requested for transfer or -1 if not explicit + */ + public int getPort() { + return port; + } + + /** + * Return a new URI matching this one, but with a different port. + * + * @param n + * the new value for port. + * @return a new URI with the updated value. + */ + public URIish setPort(final int n) { + final URIish r = new URIish(this); + r.port = n > 0 ? n : -1; + return r; + } + + public int hashCode() { + int hc = 0; + if (getScheme() != null) + hc = hc * 31 + getScheme().hashCode(); + if (getUser() != null) + hc = hc * 31 + getUser().hashCode(); + if (getPass() != null) + hc = hc * 31 + getPass().hashCode(); + if (getHost() != null) + hc = hc * 31 + getHost().hashCode(); + if (getPort() > 0) + hc = hc * 31 + getPort(); + if (getPath() != null) + hc = hc * 31 + getPath().hashCode(); + return hc; + } + + public boolean equals(final Object obj) { + if (!(obj instanceof URIish)) + return false; + final URIish b = (URIish) obj; + if (!eq(getScheme(), b.getScheme())) + return false; + if (!eq(getUser(), b.getUser())) + return false; + if (!eq(getPass(), b.getPass())) + return false; + if (!eq(getHost(), b.getHost())) + return false; + if (getPort() != b.getPort()) + return false; + if (!eq(getPath(), b.getPath())) + return false; + return true; + } + + private static boolean eq(final String a, final String b) { + if (a == b) + return true; + if (a == null || b == null) + return false; + return a.equals(b); + } + + /** + * Obtain the string form of the URI, with the password included. + * + * @return the URI, including its password field, if any. + */ + public String toPrivateString() { + return format(true); + } + + public String toString() { + return format(false); + } + + private String format(final boolean includePassword) { + final StringBuilder r = new StringBuilder(); + if (getScheme() != null) { + r.append(getScheme()); + r.append("://"); + } + + if (getUser() != null) { + r.append(getUser()); + if (includePassword && getPass() != null) { + r.append(':'); + r.append(getPass()); + } + } + + if (getHost() != null) { + if (getUser() != null) + r.append('@'); + r.append(getHost()); + if (getScheme() != null && getPort() > 0) { + r.append(':'); + r.append(getPort()); + } + } + + if (getPath() != null) { + if (getScheme() != null) { + if (!getPath().startsWith("/")) + r.append('/'); + } else if (getHost() != null) + r.append(':'); + r.append(getPath()); + } + + return r.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java new file mode 100644 index 0000000000..7e534a39c9 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java @@ -0,0 +1,486 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.BufferedOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.errors.PackProtocolException; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PackWriter; +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.RevFlag; +import org.eclipse.jgit.revwalk.RevFlagSet; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.util.io.InterruptTimer; +import org.eclipse.jgit.util.io.TimeoutInputStream; +import org.eclipse.jgit.util.io.TimeoutOutputStream; + +/** + * Implements the server side of a fetch connection, transmitting objects. + */ +public class UploadPack { + static final String OPTION_INCLUDE_TAG = BasePackFetchConnection.OPTION_INCLUDE_TAG; + + static final String OPTION_MULTI_ACK = BasePackFetchConnection.OPTION_MULTI_ACK; + + static final String OPTION_THIN_PACK = BasePackFetchConnection.OPTION_THIN_PACK; + + static final String OPTION_SIDE_BAND = BasePackFetchConnection.OPTION_SIDE_BAND; + + static final String OPTION_SIDE_BAND_64K = BasePackFetchConnection.OPTION_SIDE_BAND_64K; + + static final String OPTION_OFS_DELTA = BasePackFetchConnection.OPTION_OFS_DELTA; + + static final String OPTION_NO_PROGRESS = BasePackFetchConnection.OPTION_NO_PROGRESS; + + /** Database we read the objects from. */ + private final Repository db; + + /** Revision traversal support over {@link #db}. */ + private final RevWalk walk; + + /** Timeout in seconds to wait for client interaction. */ + private int timeout; + + /** Timer to manage {@link #timeout}. */ + private InterruptTimer timer; + + private InputStream rawIn; + + private OutputStream rawOut; + + private PacketLineIn pckIn; + + private PacketLineOut pckOut; + + /** The refs we advertised as existing at the start of the connection. */ + private Map<String, Ref> refs; + + /** Capabilities requested by the client. */ + private final Set<String> options = new HashSet<String>(); + + /** Objects the client wants to obtain. */ + private final List<RevObject> wantAll = new ArrayList<RevObject>(); + + /** Objects the client wants to obtain. */ + private final List<RevCommit> wantCommits = new ArrayList<RevCommit>(); + + /** Objects on both sides, these don't have to be sent. */ + private final List<RevObject> commonBase = new ArrayList<RevObject>(); + + /** null if {@link #commonBase} should be examined again. */ + private Boolean okToGiveUp; + + /** Marked on objects we sent in our advertisement list. */ + private final RevFlag ADVERTISED; + + /** Marked on objects the client has asked us to give them. */ + private final RevFlag WANT; + + /** Marked on objects both we and the client have. */ + private final RevFlag PEER_HAS; + + /** Marked on objects in {@link #commonBase}. */ + private final RevFlag COMMON; + + private final RevFlagSet SAVE; + + private boolean multiAck; + + /** + * Create a new pack upload for an open repository. + * + * @param copyFrom + * the source repository. + */ + public UploadPack(final Repository copyFrom) { + db = copyFrom; + walk = new RevWalk(db); + walk.setRetainBody(false); + + ADVERTISED = walk.newFlag("ADVERTISED"); + WANT = walk.newFlag("WANT"); + PEER_HAS = walk.newFlag("PEER_HAS"); + COMMON = walk.newFlag("COMMON"); + walk.carry(PEER_HAS); + + SAVE = new RevFlagSet(); + SAVE.add(ADVERTISED); + SAVE.add(WANT); + SAVE.add(PEER_HAS); + } + + /** @return the repository this receive completes into. */ + public final Repository getRepository() { + return db; + } + + /** @return the RevWalk instance used by this connection. */ + public final RevWalk getRevWalk() { + return walk; + } + + /** @return timeout (in seconds) before aborting an IO operation. */ + public int getTimeout() { + return timeout; + } + + /** + * Set the timeout before willing to abort an IO call. + * + * @param seconds + * number of seconds to wait (with no data transfer occurring) + * before aborting an IO read or write operation with the + * connected client. + */ + public void setTimeout(final int seconds) { + timeout = seconds; + } + + /** + * Execute the upload task on the socket. + * + * @param input + * raw input to read client commands from. Caller must ensure the + * input is buffered, otherwise read performance may suffer. + * @param output + * response back to the Git network client, to write the pack + * data onto. Caller must ensure the output is buffered, + * otherwise write performance may suffer. + * @param messages + * secondary "notice" channel to send additional messages out + * through. When run over SSH this should be tied back to the + * standard error channel of the command execution. For most + * other network connections this should be null. + * @throws IOException + */ + public void upload(final InputStream input, final OutputStream output, + final OutputStream messages) throws IOException { + try { + rawIn = input; + rawOut = output; + + if (timeout > 0) { + final Thread caller = Thread.currentThread(); + timer = new InterruptTimer(caller.getName() + "-Timer"); + TimeoutInputStream i = new TimeoutInputStream(rawIn, timer); + TimeoutOutputStream o = new TimeoutOutputStream(rawOut, timer); + i.setTimeout(timeout * 1000); + o.setTimeout(timeout * 1000); + rawIn = i; + rawOut = o; + } + + pckIn = new PacketLineIn(rawIn); + pckOut = new PacketLineOut(rawOut); + service(); + } finally { + if (timer != null) { + try { + timer.terminate(); + } finally { + timer = null; + } + } + } + } + + private void service() throws IOException { + sendAdvertisedRefs(); + recvWants(); + if (wantAll.isEmpty()) + return; + multiAck = options.contains(OPTION_MULTI_ACK); + negotiate(); + sendPack(); + } + + private void sendAdvertisedRefs() throws IOException { + final RefAdvertiser adv = new RefAdvertiser(pckOut, walk, ADVERTISED); + adv.advertiseCapability(OPTION_INCLUDE_TAG); + adv.advertiseCapability(OPTION_MULTI_ACK); + adv.advertiseCapability(OPTION_OFS_DELTA); + adv.advertiseCapability(OPTION_SIDE_BAND); + adv.advertiseCapability(OPTION_SIDE_BAND_64K); + adv.advertiseCapability(OPTION_THIN_PACK); + adv.advertiseCapability(OPTION_NO_PROGRESS); + adv.setDerefTags(true); + refs = db.getAllRefs(); + adv.send(refs.values()); + pckOut.end(); + } + + private void recvWants() throws IOException { + boolean isFirst = true; + for (;; isFirst = false) { + String line; + try { + line = pckIn.readString(); + } catch (EOFException eof) { + if (isFirst) + break; + throw eof; + } + + if (line == PacketLineIn.END) + break; + if (!line.startsWith("want ") || line.length() < 45) + throw new PackProtocolException("expected want; got " + line); + + if (isFirst && line.length() > 45) { + String opt = line.substring(45); + if (opt.startsWith(" ")) + opt = opt.substring(1); + for (String c : opt.split(" ")) + options.add(c); + line = line.substring(0, 45); + } + + final ObjectId id = ObjectId.fromString(line.substring(5)); + final RevObject o; + try { + o = walk.parseAny(id); + } catch (IOException e) { + throw new PackProtocolException(id.name() + " not valid", e); + } + if (!o.has(ADVERTISED)) + throw new PackProtocolException(id.name() + " not valid"); + want(o); + } + } + + private void want(RevObject o) { + if (!o.has(WANT)) { + o.add(WANT); + wantAll.add(o); + + if (o instanceof RevCommit) + wantCommits.add((RevCommit) o); + + else if (o instanceof RevTag) { + do { + o = ((RevTag) o).getObject(); + } while (o instanceof RevTag); + if (o instanceof RevCommit) + want(o); + } + } + } + + private void negotiate() throws IOException { + ObjectId last = ObjectId.zeroId(); + for (;;) { + String line; + try { + line = pckIn.readString(); + } catch (EOFException eof) { + throw eof; + } + + if (line == PacketLineIn.END) { + if (commonBase.isEmpty() || multiAck) + pckOut.writeString("NAK\n"); + pckOut.flush(); + } else if (line.startsWith("have ") && line.length() == 45) { + final ObjectId id = ObjectId.fromString(line.substring(5)); + if (matchHave(id)) { + // Both sides have the same object; let the client know. + // + if (multiAck) { + last = id; + pckOut.writeString("ACK " + id.name() + " continue\n"); + } else if (commonBase.size() == 1) + pckOut.writeString("ACK " + id.name() + "\n"); + } else { + // They have this object; we don't. + // + if (multiAck && okToGiveUp()) + pckOut.writeString("ACK " + id.name() + " continue\n"); + } + + } else if (line.equals("done")) { + if (commonBase.isEmpty()) + pckOut.writeString("NAK\n"); + + else if (multiAck) + pckOut.writeString("ACK " + last.name() + "\n"); + break; + + } else { + throw new PackProtocolException("expected have; got " + line); + } + } + } + + private boolean matchHave(final ObjectId id) { + final RevObject o; + try { + o = walk.parseAny(id); + } catch (IOException err) { + return false; + } + + if (!o.has(PEER_HAS)) { + o.add(PEER_HAS); + if (o instanceof RevCommit) + ((RevCommit) o).carry(PEER_HAS); + addCommonBase(o); + } + return true; + } + + private void addCommonBase(final RevObject o) { + if (!o.has(COMMON)) { + o.add(COMMON); + commonBase.add(o); + okToGiveUp = null; + } + } + + private boolean okToGiveUp() throws PackProtocolException { + if (okToGiveUp == null) + okToGiveUp = Boolean.valueOf(okToGiveUpImp()); + return okToGiveUp.booleanValue(); + } + + private boolean okToGiveUpImp() throws PackProtocolException { + if (commonBase.isEmpty()) + return false; + + try { + for (final Iterator<RevCommit> i = wantCommits.iterator(); i + .hasNext();) { + final RevCommit want = i.next(); + if (wantSatisfied(want)) + i.remove(); + } + } catch (IOException e) { + throw new PackProtocolException("internal revision error", e); + } + return wantCommits.isEmpty(); + } + + private boolean wantSatisfied(final RevCommit want) throws IOException { + walk.resetRetain(SAVE); + walk.markStart(want); + for (;;) { + final RevCommit c = walk.next(); + if (c == null) + break; + if (c.has(PEER_HAS)) { + addCommonBase(c); + return true; + } + } + return false; + } + + private void sendPack() throws IOException { + final boolean thin = options.contains(OPTION_THIN_PACK); + final boolean progress = !options.contains(OPTION_NO_PROGRESS); + final boolean sideband = options.contains(OPTION_SIDE_BAND) + || options.contains(OPTION_SIDE_BAND_64K); + + ProgressMonitor pm = NullProgressMonitor.INSTANCE; + OutputStream packOut = rawOut; + + if (sideband) { + int bufsz = SideBandOutputStream.SMALL_BUF; + if (options.contains(OPTION_SIDE_BAND_64K)) + bufsz = SideBandOutputStream.MAX_BUF; + bufsz -= SideBandOutputStream.HDR_SIZE; + + packOut = new BufferedOutputStream(new SideBandOutputStream( + SideBandOutputStream.CH_DATA, pckOut), bufsz); + + if (progress) + pm = new SideBandProgressMonitor(pckOut); + } + + final PackWriter pw; + pw = new PackWriter(db, pm, NullProgressMonitor.INSTANCE); + pw.setDeltaBaseAsOffset(options.contains(OPTION_OFS_DELTA)); + pw.setThin(thin); + pw.preparePack(wantAll, commonBase); + if (options.contains(OPTION_INCLUDE_TAG)) { + for (final Ref r : refs.values()) { + final RevObject o; + try { + o = walk.parseAny(r.getObjectId()); + } catch (IOException e) { + continue; + } + if (o.has(WANT) || !(o instanceof RevTag)) + continue; + final RevTag t = (RevTag) o; + if (!pw.willInclude(t) && pw.willInclude(t.getObject())) + pw.addObject(t); + } + } + pw.writePack(packOut); + + if (sideband) { + packOut.flush(); + pckOut.end(); + } else { + rawOut.flush(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java new file mode 100644 index 0000000000..d368fb2cd7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.PBEParameterSpec; + +abstract class WalkEncryption { + static final WalkEncryption NONE = new NoEncryption(); + + static final String JETS3T_CRYPTO_VER = "jets3t-crypto-ver"; + + static final String JETS3T_CRYPTO_ALG = "jets3t-crypto-alg"; + + abstract OutputStream encrypt(OutputStream os) throws IOException; + + abstract InputStream decrypt(InputStream in) throws IOException; + + abstract void request(HttpURLConnection u, String prefix); + + abstract void validate(HttpURLConnection u, String p) throws IOException; + + protected void validateImpl(final HttpURLConnection u, final String p, + final String version, final String name) throws IOException { + String v; + + v = u.getHeaderField(p + JETS3T_CRYPTO_VER); + if (v == null) + v = ""; + if (!version.equals(v)) + throw new IOException("Unsupported encryption version: " + v); + + v = u.getHeaderField(p + JETS3T_CRYPTO_ALG); + if (v == null) + v = ""; + if (!name.equals(v)) + throw new IOException("Unsupported encryption algorithm: " + v); + } + + IOException error(final Throwable why) { + final IOException e; + e = new IOException("Encryption error: " + why.getMessage()); + e.initCause(why); + return e; + } + + private static class NoEncryption extends WalkEncryption { + @Override + void request(HttpURLConnection u, String prefix) { + // Don't store any request properties. + } + + @Override + void validate(final HttpURLConnection u, final String p) + throws IOException { + validateImpl(u, p, "", ""); + } + + @Override + InputStream decrypt(InputStream in) { + return in; + } + + @Override + OutputStream encrypt(OutputStream os) { + return os; + } + } + + static class ObjectEncryptionV2 extends WalkEncryption { + private static int ITERATION_COUNT = 5000; + + private static byte[] salt = { (byte) 0xA4, (byte) 0x0B, (byte) 0xC8, + (byte) 0x34, (byte) 0xD6, (byte) 0x95, (byte) 0xF3, (byte) 0x13 }; + + private final String algorithmName; + + private final SecretKey skey; + + private final PBEParameterSpec aspec; + + ObjectEncryptionV2(final String algo, final String key) + throws InvalidKeySpecException, NoSuchAlgorithmException { + algorithmName = algo; + + final PBEKeySpec s; + s = new PBEKeySpec(key.toCharArray(), salt, ITERATION_COUNT, 32); + skey = SecretKeyFactory.getInstance(algo).generateSecret(s); + aspec = new PBEParameterSpec(salt, ITERATION_COUNT); + } + + @Override + void request(final HttpURLConnection u, final String prefix) { + u.setRequestProperty(prefix + JETS3T_CRYPTO_VER, "2"); + u.setRequestProperty(prefix + JETS3T_CRYPTO_ALG, algorithmName); + } + + @Override + void validate(final HttpURLConnection u, final String p) + throws IOException { + validateImpl(u, p, "2", algorithmName); + } + + @Override + OutputStream encrypt(final OutputStream os) throws IOException { + try { + final Cipher c = Cipher.getInstance(algorithmName); + c.init(Cipher.ENCRYPT_MODE, skey, aspec); + return new CipherOutputStream(os, c); + } catch (NoSuchAlgorithmException e) { + throw error(e); + } catch (NoSuchPaddingException e) { + throw error(e); + } catch (InvalidKeyException e) { + throw error(e); + } catch (InvalidAlgorithmParameterException e) { + throw error(e); + } + } + + @Override + InputStream decrypt(final InputStream in) throws IOException { + try { + final Cipher c = Cipher.getInstance(algorithmName); + c.init(Cipher.DECRYPT_MODE, skey, aspec); + return new CipherInputStream(in, c); + } catch (NoSuchAlgorithmException e) { + throw error(e); + } catch (NoSuchPaddingException e) { + throw error(e); + } catch (InvalidKeyException e) { + throw error(e); + } catch (InvalidAlgorithmParameterException e) { + throw error(e); + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java new file mode 100644 index 0000000000..8660a195d5 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java @@ -0,0 +1,878 @@ +/* + * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.eclipse.jgit.errors.CompoundException; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.ObjectWritingException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.MutableObjectId; +import org.eclipse.jgit.lib.ObjectChecker; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PackIndex; +import org.eclipse.jgit.lib.PackLock; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.UnpackedObjectLoader; +import org.eclipse.jgit.revwalk.DateRevQueue; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; + +/** + * Generic fetch support for dumb transport protocols. + * <p> + * Since there are no Git-specific smarts on the remote side of the connection + * the client side must determine which objects it needs to copy in order to + * completely fetch the requested refs and their history. The generic walk + * support in this class parses each individual object (once it has been copied + * to the local repository) and examines the list of objects that must also be + * copied to create a complete history. Objects which are already available + * locally are retained (and not copied), saving bandwidth for incremental + * fetches. Pack files are copied from the remote repository only as a last + * resort, as the entire pack must be copied locally in order to access any + * single object. + * <p> + * This fetch connection does not actually perform the object data transfer. + * Instead it delegates the transfer to a {@link WalkRemoteObjectDatabase}, + * which knows how to read individual files from the remote repository and + * supply the data as a standard Java InputStream. + * + * @see WalkRemoteObjectDatabase + */ +class WalkFetchConnection extends BaseFetchConnection { + /** The repository this transport fetches into, or pushes out of. */ + private final Repository local; + + /** If not null the validator for received objects. */ + private final ObjectChecker objCheck; + + /** + * List of all remote repositories we may need to get objects out of. + * <p> + * The first repository in the list is the one we were asked to fetch from; + * the remaining repositories point to the alternate locations we can fetch + * objects through. + */ + private final List<WalkRemoteObjectDatabase> remotes; + + /** Most recently used item in {@link #remotes}. */ + private int lastRemoteIdx; + + private final RevWalk revWalk; + + private final TreeWalk treeWalk; + + /** Objects whose direct dependents we know we have (or will have). */ + private final RevFlag COMPLETE; + + /** Objects that have already entered {@link #workQueue}. */ + private final RevFlag IN_WORK_QUEUE; + + /** Commits that have already entered {@link #localCommitQueue}. */ + private final RevFlag LOCALLY_SEEN; + + /** Commits already reachable from all local refs. */ + private final DateRevQueue localCommitQueue; + + /** Objects we need to copy from the remote repository. */ + private LinkedList<ObjectId> workQueue; + + /** Databases we have not yet obtained the list of packs from. */ + private final LinkedList<WalkRemoteObjectDatabase> noPacksYet; + + /** Databases we have not yet obtained the alternates from. */ + private final LinkedList<WalkRemoteObjectDatabase> noAlternatesYet; + + /** Packs we have discovered, but have not yet fetched locally. */ + private final LinkedList<RemotePack> unfetchedPacks; + + /** + * Packs whose indexes we have looked at in {@link #unfetchedPacks}. + * <p> + * We try to avoid getting duplicate copies of the same pack through + * multiple alternates by only looking at packs whose names are not yet in + * this collection. + */ + private final Set<String> packsConsidered; + + private final MutableObjectId idBuffer = new MutableObjectId(); + + private final MessageDigest objectDigest = Constants.newMessageDigest(); + + /** + * Errors received while trying to obtain an object. + * <p> + * If the fetch winds up failing because we cannot locate a specific object + * then we need to report all errors related to that object back to the + * caller as there may be cascading failures. + */ + private final HashMap<ObjectId, List<Throwable>> fetchErrors; + + private String lockMessage; + + private final List<PackLock> packLocks; + + WalkFetchConnection(final WalkTransport t, final WalkRemoteObjectDatabase w) { + Transport wt = (Transport)t; + local = wt.local; + objCheck = wt.isCheckFetchedObjects() ? new ObjectChecker() : null; + + remotes = new ArrayList<WalkRemoteObjectDatabase>(); + remotes.add(w); + + unfetchedPacks = new LinkedList<RemotePack>(); + packsConsidered = new HashSet<String>(); + + noPacksYet = new LinkedList<WalkRemoteObjectDatabase>(); + noPacksYet.add(w); + + noAlternatesYet = new LinkedList<WalkRemoteObjectDatabase>(); + noAlternatesYet.add(w); + + fetchErrors = new HashMap<ObjectId, List<Throwable>>(); + packLocks = new ArrayList<PackLock>(4); + + revWalk = new RevWalk(local); + revWalk.setRetainBody(false); + treeWalk = new TreeWalk(local); + COMPLETE = revWalk.newFlag("COMPLETE"); + IN_WORK_QUEUE = revWalk.newFlag("IN_WORK_QUEUE"); + LOCALLY_SEEN = revWalk.newFlag("LOCALLY_SEEN"); + + localCommitQueue = new DateRevQueue(); + workQueue = new LinkedList<ObjectId>(); + } + + public boolean didFetchTestConnectivity() { + return true; + } + + @Override + protected void doFetch(final ProgressMonitor monitor, + final Collection<Ref> want, final Set<ObjectId> have) + throws TransportException { + markLocalRefsComplete(have); + queueWants(want); + + while (!monitor.isCancelled() && !workQueue.isEmpty()) { + final ObjectId id = workQueue.removeFirst(); + if (!(id instanceof RevObject) || !((RevObject) id).has(COMPLETE)) + downloadObject(monitor, id); + process(id); + } + } + + public Collection<PackLock> getPackLocks() { + return packLocks; + } + + public void setPackLockMessage(final String message) { + lockMessage = message; + } + + @Override + public void close() { + for (final RemotePack p : unfetchedPacks) + p.tmpIdx.delete(); + for (final WalkRemoteObjectDatabase r : remotes) + r.close(); + } + + private void queueWants(final Collection<Ref> want) + throws TransportException { + final HashSet<ObjectId> inWorkQueue = new HashSet<ObjectId>(); + for (final Ref r : want) { + final ObjectId id = r.getObjectId(); + try { + final RevObject obj = revWalk.parseAny(id); + if (obj.has(COMPLETE)) + continue; + if (inWorkQueue.add(id)) { + obj.add(IN_WORK_QUEUE); + workQueue.add(obj); + } + } catch (MissingObjectException e) { + if (inWorkQueue.add(id)) + workQueue.add(id); + } catch (IOException e) { + throw new TransportException("Cannot read " + id.name(), e); + } + } + } + + private void process(final ObjectId id) throws TransportException { + final RevObject obj; + try { + if (id instanceof RevObject) { + obj = (RevObject) id; + if (obj.has(COMPLETE)) + return; + revWalk.parseHeaders(obj); + } else { + obj = revWalk.parseAny(id); + if (obj.has(COMPLETE)) + return; + } + } catch (IOException e) { + throw new TransportException("Cannot read " + id.name(), e); + } + + switch (obj.getType()) { + case Constants.OBJ_BLOB: + processBlob(obj); + break; + case Constants.OBJ_TREE: + processTree(obj); + break; + case Constants.OBJ_COMMIT: + processCommit(obj); + break; + case Constants.OBJ_TAG: + processTag(obj); + break; + default: + throw new TransportException("Unknown object type " + id.name()); + } + + // If we had any prior errors fetching this object they are + // now resolved, as the object was parsed successfully. + // + fetchErrors.remove(id.copy()); + } + + private void processBlob(final RevObject obj) throws TransportException { + if (!local.hasObject(obj)) + throw new TransportException("Cannot read blob " + obj.name(), + new MissingObjectException(obj, Constants.TYPE_BLOB)); + obj.add(COMPLETE); + } + + private void processTree(final RevObject obj) throws TransportException { + try { + treeWalk.reset(obj); + while (treeWalk.next()) { + final FileMode mode = treeWalk.getFileMode(0); + final int sType = mode.getObjectType(); + + switch (sType) { + case Constants.OBJ_BLOB: + case Constants.OBJ_TREE: + treeWalk.getObjectId(idBuffer, 0); + needs(revWalk.lookupAny(idBuffer, sType)); + continue; + + default: + if (FileMode.GITLINK.equals(mode)) + continue; + treeWalk.getObjectId(idBuffer, 0); + throw new CorruptObjectException("Invalid mode " + mode + + " for " + idBuffer.name() + " " + + treeWalk.getPathString() + " in " + + obj.getId().name() + "."); + } + } + } catch (IOException ioe) { + throw new TransportException("Cannot read tree " + obj.name(), ioe); + } + obj.add(COMPLETE); + } + + private void processCommit(final RevObject obj) throws TransportException { + final RevCommit commit = (RevCommit) obj; + markLocalCommitsComplete(commit.getCommitTime()); + needs(commit.getTree()); + for (final RevCommit p : commit.getParents()) + needs(p); + obj.add(COMPLETE); + } + + private void processTag(final RevObject obj) { + final RevTag tag = (RevTag) obj; + needs(tag.getObject()); + obj.add(COMPLETE); + } + + private void needs(final RevObject obj) { + if (obj.has(COMPLETE)) + return; + if (!obj.has(IN_WORK_QUEUE)) { + obj.add(IN_WORK_QUEUE); + workQueue.add(obj); + } + } + + private void downloadObject(final ProgressMonitor pm, final AnyObjectId id) + throws TransportException { + if (local.hasObject(id)) + return; + + for (;;) { + // Try a pack file we know about, but don't have yet. Odds are + // that if it has this object, it has others related to it so + // getting the pack is a good bet. + // + if (downloadPackedObject(pm, id)) + return; + + // Search for a loose object over all alternates, starting + // from the one we last successfully located an object through. + // + final String idStr = id.name(); + final String subdir = idStr.substring(0, 2); + final String file = idStr.substring(2); + final String looseName = subdir + "/" + file; + + for (int i = lastRemoteIdx; i < remotes.size(); i++) { + if (downloadLooseObject(id, looseName, remotes.get(i))) { + lastRemoteIdx = i; + return; + } + } + for (int i = 0; i < lastRemoteIdx; i++) { + if (downloadLooseObject(id, looseName, remotes.get(i))) { + lastRemoteIdx = i; + return; + } + } + + // Try to obtain more pack information and search those. + // + while (!noPacksYet.isEmpty()) { + final WalkRemoteObjectDatabase wrr = noPacksYet.removeFirst(); + final Collection<String> packNameList; + try { + pm.beginTask("Listing packs", ProgressMonitor.UNKNOWN); + packNameList = wrr.getPackNames(); + } catch (IOException e) { + // Try another repository. + // + recordError(id, e); + continue; + } finally { + pm.endTask(); + } + + if (packNameList == null || packNameList.isEmpty()) + continue; + for (final String packName : packNameList) { + if (packsConsidered.add(packName)) + unfetchedPacks.add(new RemotePack(wrr, packName)); + } + if (downloadPackedObject(pm, id)) + return; + } + + // Try to expand the first alternate we haven't expanded yet. + // + Collection<WalkRemoteObjectDatabase> al = expandOneAlternate(id, pm); + if (al != null && !al.isEmpty()) { + for (final WalkRemoteObjectDatabase alt : al) { + remotes.add(alt); + noPacksYet.add(alt); + noAlternatesYet.add(alt); + } + continue; + } + + // We could not obtain the object. There may be reasons why. + // + List<Throwable> failures = fetchErrors.get(id.copy()); + final TransportException te; + + te = new TransportException("Cannot get " + id.name() + "."); + if (failures != null && !failures.isEmpty()) { + if (failures.size() == 1) + te.initCause(failures.get(0)); + else + te.initCause(new CompoundException(failures)); + } + throw te; + } + } + + private boolean downloadPackedObject(final ProgressMonitor monitor, + final AnyObjectId id) throws TransportException { + // Search for the object in a remote pack whose index we have, + // but whose pack we do not yet have. + // + final Iterator<RemotePack> packItr = unfetchedPacks.iterator(); + while (packItr.hasNext() && !monitor.isCancelled()) { + final RemotePack pack = packItr.next(); + try { + pack.openIndex(monitor); + } catch (IOException err) { + // If the index won't open its either not found or + // its a format we don't recognize. In either case + // we may still be able to obtain the object from + // another source, so don't consider it a failure. + // + recordError(id, err); + packItr.remove(); + continue; + } + + if (monitor.isCancelled()) { + // If we were cancelled while the index was opening + // the open may have aborted. We can't search an + // unopen index. + // + return false; + } + + if (!pack.index.hasObject(id)) { + // Not in this pack? Try another. + // + continue; + } + + // It should be in the associated pack. Download that + // and attach it to the local repository so we can use + // all of the contained objects. + // + try { + pack.downloadPack(monitor); + } catch (IOException err) { + // If the pack failed to download, index correctly, + // or open in the local repository we may still be + // able to obtain this object from another pack or + // an alternate. + // + recordError(id, err); + continue; + } finally { + // If the pack was good its in the local repository + // and Repository.hasObject(id) will succeed in the + // future, so we do not need this data anymore. If + // it failed the index and pack are unusable and we + // shouldn't consult them again. + // + pack.tmpIdx.delete(); + packItr.remove(); + } + + if (!local.hasObject(id)) { + // What the hell? This pack claimed to have + // the object, but after indexing we didn't + // actually find it in the pack. + // + recordError(id, new FileNotFoundException("Object " + id.name() + + " not found in " + pack.packName + ".")); + continue; + } + + // Complete any other objects that we can. + // + final Iterator<ObjectId> pending = swapFetchQueue(); + while (pending.hasNext()) { + final ObjectId p = pending.next(); + if (pack.index.hasObject(p)) { + pending.remove(); + process(p); + } else { + workQueue.add(p); + } + } + return true; + + } + return false; + } + + private Iterator<ObjectId> swapFetchQueue() { + final Iterator<ObjectId> r = workQueue.iterator(); + workQueue = new LinkedList<ObjectId>(); + return r; + } + + private boolean downloadLooseObject(final AnyObjectId id, + final String looseName, final WalkRemoteObjectDatabase remote) + throws TransportException { + try { + final byte[] compressed = remote.open(looseName).toArray(); + verifyLooseObject(id, compressed); + saveLooseObject(id, compressed); + return true; + } catch (FileNotFoundException e) { + // Not available in a loose format from this alternate? + // Try another strategy to get the object. + // + recordError(id, e); + return false; + } catch (IOException e) { + throw new TransportException("Cannot download " + id.name(), e); + } + } + + private void verifyLooseObject(final AnyObjectId id, final byte[] compressed) + throws IOException { + final UnpackedObjectLoader uol; + try { + uol = new UnpackedObjectLoader(compressed); + } catch (CorruptObjectException parsingError) { + // Some HTTP servers send back a "200 OK" status with an HTML + // page that explains the requested file could not be found. + // These servers are most certainly misconfigured, but many + // of them exist in the world, and many of those are hosting + // Git repositories. + // + // Since an HTML page is unlikely to hash to one of our loose + // objects we treat this condition as a FileNotFoundException + // and attempt to recover by getting the object from another + // source. + // + final FileNotFoundException e; + e = new FileNotFoundException(id.name()); + e.initCause(parsingError); + throw e; + } + + objectDigest.reset(); + objectDigest.update(Constants.encodedTypeString(uol.getType())); + objectDigest.update((byte) ' '); + objectDigest.update(Constants.encodeASCII(uol.getSize())); + objectDigest.update((byte) 0); + objectDigest.update(uol.getCachedBytes()); + idBuffer.fromRaw(objectDigest.digest(), 0); + + if (!AnyObjectId.equals(id, idBuffer)) { + throw new TransportException("Incorrect hash for " + id.name() + + "; computed " + idBuffer.name() + " as a " + + Constants.typeString(uol.getType()) + " from " + + compressed.length + " bytes."); + } + if (objCheck != null) { + try { + objCheck.check(uol.getType(), uol.getCachedBytes()); + } catch (CorruptObjectException e) { + throw new TransportException("Invalid " + + Constants.typeString(uol.getType()) + " " + + id.name() + ":" + e.getMessage()); + } + } + } + + private void saveLooseObject(final AnyObjectId id, final byte[] compressed) + throws IOException, ObjectWritingException { + final File tmp; + + tmp = File.createTempFile("noz", null, local.getObjectsDirectory()); + try { + final FileOutputStream out = new FileOutputStream(tmp); + try { + out.write(compressed); + } finally { + out.close(); + } + tmp.setReadOnly(); + } catch (IOException e) { + tmp.delete(); + throw e; + } + + final File o = local.toFile(id); + if (tmp.renameTo(o)) + return; + + // Maybe the directory doesn't exist yet as the object + // directories are always lazily created. Note that we + // try the rename first as the directory likely does exist. + // + o.getParentFile().mkdir(); + if (tmp.renameTo(o)) + return; + + tmp.delete(); + if (local.hasObject(id)) + return; + throw new ObjectWritingException("Unable to store " + id.name() + "."); + } + + private Collection<WalkRemoteObjectDatabase> expandOneAlternate( + final AnyObjectId id, final ProgressMonitor pm) { + while (!noAlternatesYet.isEmpty()) { + final WalkRemoteObjectDatabase wrr = noAlternatesYet.removeFirst(); + try { + pm.beginTask("Listing alternates", ProgressMonitor.UNKNOWN); + Collection<WalkRemoteObjectDatabase> altList = wrr + .getAlternates(); + if (altList != null && !altList.isEmpty()) + return altList; + } catch (IOException e) { + // Try another repository. + // + recordError(id, e); + } finally { + pm.endTask(); + } + } + return null; + } + + private void markLocalRefsComplete(final Set<ObjectId> have) throws TransportException { + for (final Ref r : local.getAllRefs().values()) { + try { + markLocalObjComplete(revWalk.parseAny(r.getObjectId())); + } catch (IOException readError) { + throw new TransportException("Local ref " + r.getName() + + " is missing object(s).", readError); + } + } + for (final ObjectId id : have) { + try { + markLocalObjComplete(revWalk.parseAny(id)); + } catch (IOException readError) { + throw new TransportException("Missing assumed "+id.name(), readError); + } + } + } + + private void markLocalObjComplete(RevObject obj) throws IOException { + while (obj.getType() == Constants.OBJ_TAG) { + obj.add(COMPLETE); + obj = ((RevTag) obj).getObject(); + revWalk.parseHeaders(obj); + } + + switch (obj.getType()) { + case Constants.OBJ_BLOB: + obj.add(COMPLETE); + break; + case Constants.OBJ_COMMIT: + pushLocalCommit((RevCommit) obj); + break; + case Constants.OBJ_TREE: + markTreeComplete((RevTree) obj); + break; + } + } + + private void markLocalCommitsComplete(final int until) + throws TransportException { + try { + for (;;) { + final RevCommit c = localCommitQueue.peek(); + if (c == null || c.getCommitTime() < until) + return; + localCommitQueue.next(); + + markTreeComplete(c.getTree()); + for (final RevCommit p : c.getParents()) + pushLocalCommit(p); + } + } catch (IOException err) { + throw new TransportException("Local objects incomplete.", err); + } + } + + private void pushLocalCommit(final RevCommit p) + throws MissingObjectException, IOException { + if (p.has(LOCALLY_SEEN)) + return; + revWalk.parseHeaders(p); + p.add(LOCALLY_SEEN); + p.add(COMPLETE); + p.carry(COMPLETE); + localCommitQueue.add(p); + } + + private void markTreeComplete(final RevTree tree) throws IOException { + if (tree.has(COMPLETE)) + return; + tree.add(COMPLETE); + treeWalk.reset(tree); + while (treeWalk.next()) { + final FileMode mode = treeWalk.getFileMode(0); + final int sType = mode.getObjectType(); + + switch (sType) { + case Constants.OBJ_BLOB: + treeWalk.getObjectId(idBuffer, 0); + revWalk.lookupAny(idBuffer, sType).add(COMPLETE); + continue; + + case Constants.OBJ_TREE: { + treeWalk.getObjectId(idBuffer, 0); + final RevObject o = revWalk.lookupAny(idBuffer, sType); + if (!o.has(COMPLETE)) { + o.add(COMPLETE); + treeWalk.enterSubtree(); + } + continue; + } + default: + if (FileMode.GITLINK.equals(mode)) + continue; + treeWalk.getObjectId(idBuffer, 0); + throw new CorruptObjectException("Invalid mode " + mode + + " for " + idBuffer.name() + " " + + treeWalk.getPathString() + " in " + tree.name() + "."); + } + } + } + + private void recordError(final AnyObjectId id, final Throwable what) { + final ObjectId objId = id.copy(); + List<Throwable> errors = fetchErrors.get(objId); + if (errors == null) { + errors = new ArrayList<Throwable>(2); + fetchErrors.put(objId, errors); + } + errors.add(what); + } + + private class RemotePack { + final WalkRemoteObjectDatabase connection; + + final String packName; + + final String idxName; + + final File tmpIdx; + + PackIndex index; + + RemotePack(final WalkRemoteObjectDatabase c, final String pn) { + final File objdir = local.getObjectsDirectory(); + connection = c; + packName = pn; + idxName = packName.substring(0, packName.length() - 5) + ".idx"; + + String tn = idxName; + if (tn.startsWith("pack-")) + tn = tn.substring(5); + if (tn.endsWith(".idx")) + tn = tn.substring(0, tn.length() - 4); + tmpIdx = new File(objdir, "walk-" + tn + ".walkidx"); + } + + void openIndex(final ProgressMonitor pm) throws IOException { + if (index != null) + return; + if (tmpIdx.isFile()) { + try { + index = PackIndex.open(tmpIdx); + return; + } catch (FileNotFoundException err) { + // Fall through and get the file. + } + } + + final WalkRemoteObjectDatabase.FileStream s; + s = connection.open("pack/" + idxName); + pm.beginTask("Get " + idxName.substring(0, 12) + "..idx", + s.length < 0 ? ProgressMonitor.UNKNOWN + : (int) (s.length / 1024)); + try { + final FileOutputStream fos = new FileOutputStream(tmpIdx); + try { + final byte[] buf = new byte[2048]; + int cnt; + while (!pm.isCancelled() && (cnt = s.in.read(buf)) >= 0) { + fos.write(buf, 0, cnt); + pm.update(cnt / 1024); + } + } finally { + fos.close(); + } + } catch (IOException err) { + tmpIdx.delete(); + throw err; + } finally { + s.in.close(); + } + pm.endTask(); + + if (pm.isCancelled()) { + tmpIdx.delete(); + return; + } + + try { + index = PackIndex.open(tmpIdx); + } catch (IOException e) { + tmpIdx.delete(); + throw e; + } + } + + void downloadPack(final ProgressMonitor monitor) throws IOException { + final WalkRemoteObjectDatabase.FileStream s; + final IndexPack ip; + + s = connection.open("pack/" + packName); + ip = IndexPack.create(local, s.in); + ip.setFixThin(false); + ip.setObjectChecker(objCheck); + ip.index(monitor); + final PackLock keep = ip.renameAndOpenPack(lockMessage); + if (keep != null) + packLocks.add(keep); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java new file mode 100644 index 0000000000..56f73c50b2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import static org.eclipse.jgit.transport.WalkRemoteObjectDatabase.ROOT_DIR; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PackWriter; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefWriter; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.Ref.Storage; +import org.eclipse.jgit.transport.RemoteRefUpdate.Status; + +/** + * Generic push support for dumb transport protocols. + * <p> + * Since there are no Git-specific smarts on the remote side of the connection + * the client side must handle everything on its own. The generic push support + * requires being able to delete, create and overwrite files on the remote side, + * as well as create any missing directories (if necessary). Typically this can + * be handled through an FTP style protocol. + * <p> + * Objects not on the remote side are uploaded as pack files, using one pack + * file per invocation. This simplifies the implementation as only two data + * files need to be written to the remote repository. + * <p> + * Push support supplied by this class is not multiuser safe. Concurrent pushes + * to the same repository may yield an inconsistent reference database which may + * confuse fetch clients. + * <p> + * A single push is concurrently safe with multiple fetch requests, due to the + * careful order of operations used to update the repository. Clients fetching + * may receive transient failures due to short reads on certain files if the + * protocol does not support atomic file replacement. + * + * @see WalkRemoteObjectDatabase + */ +class WalkPushConnection extends BaseConnection implements PushConnection { + /** The repository this transport pushes out of. */ + private final Repository local; + + /** Location of the remote repository we are writing to. */ + private final URIish uri; + + /** Database connection to the remote repository. */ + private final WalkRemoteObjectDatabase dest; + + /** + * Packs already known to reside in the remote repository. + * <p> + * This is a LinkedHashMap to maintain the original order. + */ + private LinkedHashMap<String, String> packNames; + + /** Complete listing of refs the remote will have after our push. */ + private Map<String, Ref> newRefs; + + /** + * Updates which require altering the packed-refs file to complete. + * <p> + * If this collection is non-empty then any refs listed in {@link #newRefs} + * with a storage class of {@link Storage#PACKED} will be written. + */ + private Collection<RemoteRefUpdate> packedRefUpdates; + + WalkPushConnection(final WalkTransport walkTransport, + final WalkRemoteObjectDatabase w) { + Transport t = (Transport)walkTransport; + local = t.local; + uri = t.getURI(); + dest = w; + } + + public void push(final ProgressMonitor monitor, + final Map<String, RemoteRefUpdate> refUpdates) + throws TransportException { + markStartedOperation(); + packNames = null; + newRefs = new TreeMap<String, Ref>(getRefsMap()); + packedRefUpdates = new ArrayList<RemoteRefUpdate>(refUpdates.size()); + + // Filter the commands and issue all deletes first. This way we + // can correctly handle a directory being cleared out and a new + // ref using the directory name being created. + // + final List<RemoteRefUpdate> updates = new ArrayList<RemoteRefUpdate>(); + for (final RemoteRefUpdate u : refUpdates.values()) { + final String n = u.getRemoteName(); + if (!n.startsWith("refs/") || !Repository.isValidRefName(n)) { + u.setStatus(Status.REJECTED_OTHER_REASON); + u.setMessage("funny refname"); + continue; + } + + if (AnyObjectId.equals(ObjectId.zeroId(), u.getNewObjectId())) + deleteCommand(u); + else + updates.add(u); + } + + // If we have any updates we need to upload the objects first, to + // prevent creating refs pointing at non-existent data. Then we + // can update the refs, and the info-refs file for dumb transports. + // + if (!updates.isEmpty()) + sendpack(updates, monitor); + for (final RemoteRefUpdate u : updates) + updateCommand(u); + + // Is this a new repository? If so we should create additional + // metadata files so it is properly initialized during the push. + // + if (!updates.isEmpty() && isNewRepository()) + createNewRepository(updates); + + RefWriter refWriter = new RefWriter(newRefs.values()) { + @Override + protected void writeFile(String file, byte[] content) + throws IOException { + dest.writeFile(ROOT_DIR + file, content); + } + }; + if (!packedRefUpdates.isEmpty()) { + try { + refWriter.writePackedRefs(); + for (final RemoteRefUpdate u : packedRefUpdates) + u.setStatus(Status.OK); + } catch (IOException err) { + for (final RemoteRefUpdate u : packedRefUpdates) { + u.setStatus(Status.REJECTED_OTHER_REASON); + u.setMessage(err.getMessage()); + } + throw new TransportException(uri, "failed updating refs", err); + } + } + + try { + refWriter.writeInfoRefs(); + } catch (IOException err) { + throw new TransportException(uri, "failed updating refs", err); + } + } + + @Override + public void close() { + dest.close(); + } + + private void sendpack(final List<RemoteRefUpdate> updates, + final ProgressMonitor monitor) throws TransportException { + String pathPack = null; + String pathIdx = null; + + try { + final PackWriter pw = new PackWriter(local, monitor); + final List<ObjectId> need = new ArrayList<ObjectId>(); + final List<ObjectId> have = new ArrayList<ObjectId>(); + for (final RemoteRefUpdate r : updates) + need.add(r.getNewObjectId()); + for (final Ref r : getRefs()) { + have.add(r.getObjectId()); + if (r.getPeeledObjectId() != null) + have.add(r.getPeeledObjectId()); + } + pw.preparePack(need, have); + + // We don't have to continue further if the pack will + // be an empty pack, as the remote has all objects it + // needs to complete this change. + // + if (pw.getObjectsNumber() == 0) + return; + + packNames = new LinkedHashMap<String, String>(); + for (final String n : dest.getPackNames()) + packNames.put(n, n); + + final String base = "pack-" + pw.computeName().name(); + final String packName = base + ".pack"; + pathPack = "pack/" + packName; + pathIdx = "pack/" + base + ".idx"; + + if (packNames.remove(packName) != null) { + // The remote already contains this pack. We should + // remove the index before overwriting to prevent bad + // offsets from appearing to clients. + // + dest.writeInfoPacks(packNames.keySet()); + dest.deleteFile(pathIdx); + } + + // Write the pack file, then the index, as readers look the + // other direction (index, then pack file). + // + final String wt = "Put " + base.substring(0, 12); + OutputStream os = dest.writeFile(pathPack, monitor, wt + "..pack"); + try { + pw.writePack(os); + } finally { + os.close(); + } + + os = dest.writeFile(pathIdx, monitor, wt + "..idx"); + try { + pw.writeIndex(os); + } finally { + os.close(); + } + + // Record the pack at the start of the pack info list. This + // way clients are likely to consult the newest pack first, + // and discover the most recent objects there. + // + final ArrayList<String> infoPacks = new ArrayList<String>(); + infoPacks.add(packName); + infoPacks.addAll(packNames.keySet()); + dest.writeInfoPacks(infoPacks); + + } catch (IOException err) { + safeDelete(pathIdx); + safeDelete(pathPack); + + throw new TransportException(uri, "cannot store objects", err); + } + } + + private void safeDelete(final String path) { + if (path != null) { + try { + dest.deleteFile(path); + } catch (IOException cleanupFailure) { + // Ignore the deletion failure. We probably are + // already failing and were just trying to pick + // up after ourselves. + } + } + } + + private void deleteCommand(final RemoteRefUpdate u) { + final Ref r = newRefs.remove(u.getRemoteName()); + if (r == null) { + // Already gone. + // + u.setStatus(Status.OK); + return; + } + + if (r.getStorage().isPacked()) + packedRefUpdates.add(u); + + if (r.getStorage().isLoose()) { + try { + dest.deleteRef(u.getRemoteName()); + u.setStatus(Status.OK); + } catch (IOException e) { + u.setStatus(Status.REJECTED_OTHER_REASON); + u.setMessage(e.getMessage()); + } + } + + try { + dest.deleteRefLog(u.getRemoteName()); + } catch (IOException e) { + u.setStatus(Status.REJECTED_OTHER_REASON); + u.setMessage(e.getMessage()); + } + } + + private void updateCommand(final RemoteRefUpdate u) { + try { + dest.writeRef(u.getRemoteName(), u.getNewObjectId()); + newRefs.put(u.getRemoteName(), new Ref(Storage.LOOSE, u + .getRemoteName(), u.getNewObjectId())); + u.setStatus(Status.OK); + } catch (IOException e) { + u.setStatus(Status.REJECTED_OTHER_REASON); + u.setMessage(e.getMessage()); + } + } + + private boolean isNewRepository() { + return getRefsMap().isEmpty() && packNames != null + && packNames.isEmpty(); + } + + private void createNewRepository(final List<RemoteRefUpdate> updates) + throws TransportException { + try { + final String ref = "ref: " + pickHEAD(updates) + "\n"; + final byte[] bytes = Constants.encode(ref); + dest.writeFile(ROOT_DIR + Constants.HEAD, bytes); + } catch (IOException e) { + throw new TransportException(uri, "cannot create HEAD", e); + } + + try { + final String config = "[core]\n" + + "\trepositoryformatversion = 0\n"; + final byte[] bytes = Constants.encode(config); + dest.writeFile(ROOT_DIR + "config", bytes); + } catch (IOException e) { + throw new TransportException(uri, "cannot create config", e); + } + } + + private static String pickHEAD(final List<RemoteRefUpdate> updates) { + // Try to use master if the user is pushing that, it is the + // default branch and is likely what they want to remain as + // the default on the new remote. + // + for (final RemoteRefUpdate u : updates) { + final String n = u.getRemoteName(); + if (n.equals(Constants.R_HEADS + Constants.MASTER)) + return n; + } + + // Pick any branch, under the assumption the user pushed only + // one to the remote side. + // + for (final RemoteRefUpdate u : updates) { + final String n = u.getRemoteName(); + if (n.startsWith(Constants.R_HEADS)) + return n; + } + return updates.get(0).getRemoteName(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkRemoteObjectDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkRemoteObjectDatabase.java new file mode 100644 index 0000000000..6a557dfd3f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkRemoteObjectDatabase.java @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.util.NB; + +/** + * Transfers object data through a dumb transport. + * <p> + * Implementations are responsible for resolving path names relative to the + * <code>objects/</code> subdirectory of a single remote Git repository or + * naked object database and make the content available as a Java input stream + * for reading during fetch. The actual object traversal logic to determine the + * names of files to retrieve is handled through the generic, protocol + * independent {@link WalkFetchConnection}. + */ +abstract class WalkRemoteObjectDatabase { + static final String ROOT_DIR = "../"; + + static final String INFO_PACKS = "info/packs"; + + static final String INFO_ALTERNATES = "info/alternates"; + + static final String INFO_HTTP_ALTERNATES = "info/http-alternates"; + + static final String INFO_REFS = ROOT_DIR + Constants.INFO_REFS; + + abstract URIish getURI(); + + /** + * Obtain the list of available packs (if any). + * <p> + * Pack names should be the file name in the packs directory, that is + * <code>pack-035760ab452d6eebd123add421f253ce7682355a.pack</code>. Index + * names should not be included in the returned collection. + * + * @return list of pack names; null or empty list if none are available. + * @throws IOException + * The connection is unable to read the remote repository's list + * of available pack files. + */ + abstract Collection<String> getPackNames() throws IOException; + + /** + * Obtain alternate connections to alternate object databases (if any). + * <p> + * Alternates are typically read from the file {@link #INFO_ALTERNATES} or + * {@link #INFO_HTTP_ALTERNATES}. The content of each line must be resolved + * by the implementation and a new database reference should be returned to + * represent the additional location. + * <p> + * Alternates may reuse the same network connection handle, however the + * fetch connection will {@link #close()} each created alternate. + * + * @return list of additional object databases the caller could fetch from; + * null or empty list if none are configured. + * @throws IOException + * The connection is unable to read the remote repository's list + * of configured alternates. + */ + abstract Collection<WalkRemoteObjectDatabase> getAlternates() + throws IOException; + + /** + * Open a single file for reading. + * <p> + * Implementors should make every attempt possible to ensure + * {@link FileNotFoundException} is used when the remote object does not + * exist. However when fetching over HTTP some misconfigured servers may + * generate a 200 OK status message (rather than a 404 Not Found) with an + * HTML formatted message explaining the requested resource does not exist. + * Callers such as {@link WalkFetchConnection} are prepared to handle this + * by validating the content received, and assuming content that fails to + * match its hash is an incorrectly phrased FileNotFoundException. + * + * @param path + * location of the file to read, relative to this objects + * directory (e.g. + * <code>cb/95df6ab7ae9e57571511ef451cf33767c26dd2</code> or + * <code>pack/pack-035760ab452d6eebd123add421f253ce7682355a.pack</code>). + * @return a stream to read from the file. Never null. + * @throws FileNotFoundException + * the requested file does not exist at the given location. + * @throws IOException + * The connection is unable to read the remote's file, and the + * failure occurred prior to being able to determine if the file + * exists, or after it was determined to exist but before the + * stream could be created. + */ + abstract FileStream open(String path) throws FileNotFoundException, + IOException; + + /** + * Create a new connection for a discovered alternate object database + * <p> + * This method is typically called by {@link #readAlternates(String)} when + * subclasses us the generic alternate parsing logic for their + * implementation of {@link #getAlternates()}. + * + * @param location + * the location of the new alternate, relative to the current + * object database. + * @return a new database connection that can read from the specified + * alternate. + * @throws IOException + * The database connection cannot be established with the + * alternate, such as if the alternate location does not + * actually exist and the connection's constructor attempts to + * verify that. + */ + abstract WalkRemoteObjectDatabase openAlternate(String location) + throws IOException; + + /** + * Close any resources used by this connection. + * <p> + * If the remote repository is contacted by a network socket this method + * must close that network socket, disconnecting the two peers. If the + * remote repository is actually local (same system) this method must close + * any open file handles used to read the "remote" repository. + */ + abstract void close(); + + /** + * Delete a file from the object database. + * <p> + * Path may start with <code>../</code> to request deletion of a file that + * resides in the repository itself. + * <p> + * When possible empty directories must be removed, up to but not including + * the current object database directory itself. + * <p> + * This method does not support deletion of directories. + * + * @param path + * name of the item to be removed, relative to the current object + * database. + * @throws IOException + * deletion is not supported, or deletion failed. + */ + void deleteFile(final String path) throws IOException { + throw new IOException("Deleting '" + path + "' not supported."); + } + + /** + * Open a remote file for writing. + * <p> + * Path may start with <code>../</code> to request writing of a file that + * resides in the repository itself. + * <p> + * The requested path may or may not exist. If the path already exists as a + * file the file should be truncated and completely replaced. + * <p> + * This method creates any missing parent directories, if necessary. + * + * @param path + * name of the file to write, relative to the current object + * database. + * @return stream to write into this file. Caller must close the stream to + * complete the write request. The stream is not buffered and each + * write may cause a network request/response so callers should + * buffer to smooth out small writes. + * @param monitor + * (optional) progress monitor to post write completion to during + * the stream's close method. + * @param monitorTask + * (optional) task name to display during the close method. + * @throws IOException + * writing is not supported, or attempting to write the file + * failed, possibly due to permissions or remote disk full, etc. + */ + OutputStream writeFile(final String path, final ProgressMonitor monitor, + final String monitorTask) throws IOException { + throw new IOException("Writing of '" + path + "' not supported."); + } + + /** + * Atomically write a remote file. + * <p> + * This method attempts to perform as atomic of an update as it can, + * reducing (or eliminating) the time that clients might be able to see + * partial file content. This method is not suitable for very large + * transfers as the complete content must be passed as an argument. + * <p> + * Path may start with <code>../</code> to request writing of a file that + * resides in the repository itself. + * <p> + * The requested path may or may not exist. If the path already exists as a + * file the file should be truncated and completely replaced. + * <p> + * This method creates any missing parent directories, if necessary. + * + * @param path + * name of the file to write, relative to the current object + * database. + * @param data + * complete new content of the file. + * @throws IOException + * writing is not supported, or attempting to write the file + * failed, possibly due to permissions or remote disk full, etc. + */ + void writeFile(final String path, final byte[] data) throws IOException { + final OutputStream os = writeFile(path, null, null); + try { + os.write(data); + } finally { + os.close(); + } + } + + /** + * Delete a loose ref from the remote repository. + * + * @param name + * name of the ref within the ref space, for example + * <code>refs/heads/pu</code>. + * @throws IOException + * deletion is not supported, or deletion failed. + */ + void deleteRef(final String name) throws IOException { + deleteFile(ROOT_DIR + name); + } + + /** + * Delete a reflog from the remote repository. + * + * @param name + * name of the ref within the ref space, for example + * <code>refs/heads/pu</code>. + * @throws IOException + * deletion is not supported, or deletion failed. + */ + void deleteRefLog(final String name) throws IOException { + deleteFile(ROOT_DIR + Constants.LOGS + "/" + name); + } + + /** + * Overwrite (or create) a loose ref in the remote repository. + * <p> + * This method creates any missing parent directories, if necessary. + * + * @param name + * name of the ref within the ref space, for example + * <code>refs/heads/pu</code>. + * @param value + * new value to store in this ref. Must not be null. + * @throws IOException + * writing is not supported, or attempting to write the file + * failed, possibly due to permissions or remote disk full, etc. + */ + void writeRef(final String name, final ObjectId value) throws IOException { + final ByteArrayOutputStream b; + + b = new ByteArrayOutputStream(Constants.OBJECT_ID_LENGTH * 2 + 1); + value.copyTo(b); + b.write('\n'); + + writeFile(ROOT_DIR + name, b.toByteArray()); + } + + /** + * Rebuild the {@link #INFO_PACKS} for dumb transport clients. + * <p> + * This method rebuilds the contents of the {@link #INFO_PACKS} file to + * match the passed list of pack names. + * + * @param packNames + * names of available pack files, in the order they should appear + * in the file. Valid pack name strings are of the form + * <code>pack-035760ab452d6eebd123add421f253ce7682355a.pack</code>. + * @throws IOException + * writing is not supported, or attempting to write the file + * failed, possibly due to permissions or remote disk full, etc. + */ + void writeInfoPacks(final Collection<String> packNames) throws IOException { + final StringBuilder w = new StringBuilder(); + for (final String n : packNames) { + w.append("P "); + w.append(n); + w.append('\n'); + } + writeFile(INFO_PACKS, Constants.encodeASCII(w.toString())); + } + + /** + * Open a buffered reader around a file. + * <p> + * This is shorthand for calling {@link #open(String)} and then wrapping it + * in a reader suitable for line oriented files like the alternates list. + * + * @return a stream to read from the file. Never null. + * @param path + * location of the file to read, relative to this objects + * directory (e.g. <code>info/packs</code>). + * @throws FileNotFoundException + * the requested file does not exist at the given location. + * @throws IOException + * The connection is unable to read the remote's file, and the + * failure occurred prior to being able to determine if the file + * exists, or after it was determined to exist but before the + * stream could be created. + */ + BufferedReader openReader(final String path) throws IOException { + final InputStream is = open(path).in; + return new BufferedReader(new InputStreamReader(is, Constants.CHARSET)); + } + + /** + * Read a standard Git alternates file to discover other object databases. + * <p> + * This method is suitable for reading the standard formats of the + * alternates file, such as found in <code>objects/info/alternates</code> + * or <code>objects/info/http-alternates</code> within a Git repository. + * <p> + * Alternates appear one per line, with paths expressed relative to this + * object database. + * + * @param listPath + * location of the alternate file to read, relative to this + * object database (e.g. <code>info/alternates</code>). + * @return the list of discovered alternates. Empty list if the file exists, + * but no entries were discovered. + * @throws FileNotFoundException + * the requested file does not exist at the given location. + * @throws IOException + * The connection is unable to read the remote's file, and the + * failure occurred prior to being able to determine if the file + * exists, or after it was determined to exist but before the + * stream could be created. + */ + Collection<WalkRemoteObjectDatabase> readAlternates(final String listPath) + throws IOException { + final BufferedReader br = openReader(listPath); + try { + final Collection<WalkRemoteObjectDatabase> alts = new ArrayList<WalkRemoteObjectDatabase>(); + for (;;) { + String line = br.readLine(); + if (line == null) + break; + if (!line.endsWith("/")) + line += "/"; + alts.add(openAlternate(line)); + } + return alts; + } finally { + br.close(); + } + } + + /** + * Read a standard Git packed-refs file to discover known references. + * + * @param avail + * return collection of references. Any existing entries will be + * replaced if they are found in the packed-refs file. + * @throws TransportException + * an error occurred reading from the packed refs file. + */ + protected void readPackedRefs(final Map<String, Ref> avail) + throws TransportException { + try { + final BufferedReader br = openReader(ROOT_DIR + + Constants.PACKED_REFS); + try { + readPackedRefsImpl(avail, br); + } finally { + br.close(); + } + } catch (FileNotFoundException notPacked) { + // Perhaps it wasn't worthwhile, or is just an older repository. + } catch (IOException e) { + throw new TransportException(getURI(), "error in packed-refs", e); + } + } + + private void readPackedRefsImpl(final Map<String, Ref> avail, + final BufferedReader br) throws IOException { + Ref last = null; + for (;;) { + String line = br.readLine(); + if (line == null) + break; + if (line.charAt(0) == '#') + continue; + if (line.charAt(0) == '^') { + if (last == null) + throw new TransportException("Peeled line before ref."); + final ObjectId id = ObjectId.fromString(line.substring(1)); + last = new Ref(Ref.Storage.PACKED, last.getName(), last + .getObjectId(), id, true); + avail.put(last.getName(), last); + continue; + } + + final int sp = line.indexOf(' '); + if (sp < 0) + throw new TransportException("Unrecognized ref: " + line); + final ObjectId id = ObjectId.fromString(line.substring(0, sp)); + final String name = line.substring(sp + 1); + last = new Ref(Ref.Storage.PACKED, name, id); + avail.put(last.getName(), last); + } + } + + static final class FileStream { + final InputStream in; + + final long length; + + /** + * Create a new stream of unknown length. + * + * @param i + * stream containing the file data. This stream will be + * closed by the caller when reading is complete. + */ + FileStream(final InputStream i) { + in = i; + length = -1; + } + + /** + * Create a new stream of known length. + * + * @param i + * stream containing the file data. This stream will be + * closed by the caller when reading is complete. + * @param n + * total number of bytes available for reading through + * <code>i</code>. + */ + FileStream(final InputStream i, final long n) { + in = i; + length = n; + } + + byte[] toArray() throws IOException { + try { + if (length >= 0) { + final byte[] r = new byte[(int) length]; + NB.readFully(in, r, 0, r.length); + return r; + } + + final ByteArrayOutputStream r = new ByteArrayOutputStream(); + final byte[] buf = new byte[2048]; + int n; + while ((n = in.read(buf)) >= 0) + r.write(buf, 0, n); + return r.toByteArray(); + } finally { + in.close(); + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkTransport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkTransport.java new file mode 100644 index 0000000000..8b440041a2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkTransport.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> + * Copyright (C) 2009, JetBrains s.r.o. + * Copyright (C) 2008, Mike Ralphson <mike@abacus.co.uk> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.transport; + +/** + * Marker interface for an object transport walking transport. + * <p> + * Implementations of WalkTransport transfer individual objects one at a time + * from the loose objects directory, or entire packs if the source side does not + * have the object as a loose object. + * <p> + * WalkTransports are not as efficient as {@link PackTransport} instances, but + * can be useful in situations where a pack transport is not acceptable. + * + * @see WalkFetchConnection + */ +public interface WalkTransport { + // no methods in marker interface +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java new file mode 100644 index 0000000000..102974f449 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java @@ -0,0 +1,595 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.MutableObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.WindowCursor; +import org.eclipse.jgit.treewalk.filter.TreeFilter; + +/** + * Walks a Git tree (directory) in Git sort order. + * <p> + * A new iterator instance should be positioned on the first entry, or at eof. + * Data for the first entry (if not at eof) should be available immediately. + * <p> + * Implementors must walk a tree in the Git sort order, which has the following + * odd sorting: + * <ol> + * <li>A.c</li> + * <li>A/c</li> + * <li>A0c</li> + * </ol> + * <p> + * In the second item, <code>A</code> is the name of a subtree and + * <code>c</code> is a file within that subtree. The other two items are files + * in the root level tree. + * + * @see CanonicalTreeParser + */ +public abstract class AbstractTreeIterator { + /** Default size for the {@link #path} buffer. */ + protected static final int DEFAULT_PATH_SIZE = 128; + + /** A dummy object id buffer that matches the zero ObjectId. */ + protected static final byte[] zeroid = new byte[Constants.OBJECT_ID_LENGTH]; + + /** Iterator for the parent tree; null if we are the root iterator. */ + final AbstractTreeIterator parent; + + /** The iterator this current entry is path equal to. */ + AbstractTreeIterator matches; + + /** + * Number of entries we moved forward to force a D/F conflict match. + * + * @see NameConflictTreeWalk + */ + int matchShift; + + /** + * Mode bits for the current entry. + * <p> + * A numerical value from FileMode is usually faster for an iterator to + * obtain from its data source so this is the preferred representation. + * + * @see org.eclipse.jgit.lib.FileMode + */ + protected int mode; + + /** + * Path buffer for the current entry. + * <p> + * This buffer is pre-allocated at the start of walking and is shared from + * parent iterators down into their subtree iterators. The sharing allows + * the current entry to always be a full path from the root, while each + * subtree only needs to populate the part that is under their control. + */ + protected byte[] path; + + /** + * Position within {@link #path} this iterator starts writing at. + * <p> + * This is the first offset in {@link #path} that this iterator must + * populate during {@link #next}. At the root level (when {@link #parent} + * is null) this is 0. For a subtree iterator the index before this position + * should have the value '/'. + */ + protected final int pathOffset; + + /** + * Total length of the current entry's complete path from the root. + * <p> + * This is the number of bytes within {@link #path} that pertain to the + * current entry. Values at this index through the end of the array are + * garbage and may be randomly populated from prior entries. + */ + protected int pathLen; + + /** Create a new iterator with no parent. */ + protected AbstractTreeIterator() { + parent = null; + path = new byte[DEFAULT_PATH_SIZE]; + pathOffset = 0; + } + + /** + * Create a new iterator with no parent and a prefix. + * <p> + * The prefix path supplied is inserted in front of all paths generated by + * this iterator. It is intended to be used when an iterator is being + * created for a subsection of an overall repository and needs to be + * combined with other iterators that are created to run over the entire + * repository namespace. + * + * @param prefix + * position of this iterator in the repository tree. The value + * may be null or the empty string to indicate the prefix is the + * root of the repository. A trailing slash ('/') is + * automatically appended if the prefix does not end in '/'. + */ + protected AbstractTreeIterator(final String prefix) { + parent = null; + + if (prefix != null && prefix.length() > 0) { + final ByteBuffer b; + + b = Constants.CHARSET.encode(CharBuffer.wrap(prefix)); + pathLen = b.limit(); + path = new byte[Math.max(DEFAULT_PATH_SIZE, pathLen + 1)]; + b.get(path, 0, pathLen); + if (path[pathLen - 1] != '/') + path[pathLen++] = '/'; + pathOffset = pathLen; + } else { + path = new byte[DEFAULT_PATH_SIZE]; + pathOffset = 0; + } + } + + /** + * Create a new iterator with no parent and a prefix. + * <p> + * The prefix path supplied is inserted in front of all paths generated by + * this iterator. It is intended to be used when an iterator is being + * created for a subsection of an overall repository and needs to be + * combined with other iterators that are created to run over the entire + * repository namespace. + * + * @param prefix + * position of this iterator in the repository tree. The value + * may be null or the empty array to indicate the prefix is the + * root of the repository. A trailing slash ('/') is + * automatically appended if the prefix does not end in '/'. + */ + protected AbstractTreeIterator(final byte[] prefix) { + parent = null; + + if (prefix != null && prefix.length > 0) { + pathLen = prefix.length; + path = new byte[Math.max(DEFAULT_PATH_SIZE, pathLen + 1)]; + System.arraycopy(prefix, 0, path, 0, pathLen); + if (path[pathLen - 1] != '/') + path[pathLen++] = '/'; + pathOffset = pathLen; + } else { + path = new byte[DEFAULT_PATH_SIZE]; + pathOffset = 0; + } + } + + /** + * Create an iterator for a subtree of an existing iterator. + * + * @param p + * parent tree iterator. + */ + protected AbstractTreeIterator(final AbstractTreeIterator p) { + parent = p; + path = p.path; + pathOffset = p.pathLen + 1; + try { + path[pathOffset - 1] = '/'; + } catch (ArrayIndexOutOfBoundsException e) { + growPath(p.pathLen); + path[pathOffset - 1] = '/'; + } + } + + /** + * Create an iterator for a subtree of an existing iterator. + * <p> + * The caller is responsible for setting up the path of the child iterator. + * + * @param p + * parent tree iterator. + * @param childPath + * path array to be used by the child iterator. This path must + * contain the path from the top of the walk to the first child + * and must end with a '/'. + * @param childPathOffset + * position within <code>childPath</code> where the child can + * insert its data. The value at + * <code>childPath[childPathOffset-1]</code> must be '/'. + */ + protected AbstractTreeIterator(final AbstractTreeIterator p, + final byte[] childPath, final int childPathOffset) { + parent = p; + path = childPath; + pathOffset = childPathOffset; + } + + /** + * Grow the path buffer larger. + * + * @param len + * number of live bytes in the path buffer. This many bytes will + * be moved into the larger buffer. + */ + protected void growPath(final int len) { + setPathCapacity(path.length << 1, len); + } + + /** + * Ensure that path is capable to hold at least {@code capacity} bytes + * + * @param capacity + * the amount of bytes to hold + * @param len + * the amount of live bytes in path buffer + */ + protected void ensurePathCapacity(final int capacity, final int len) { + if (path.length >= capacity) + return; + final byte[] o = path; + int current = o.length; + int newCapacity = current; + while (newCapacity < capacity && newCapacity > 0) + newCapacity <<= 1; + setPathCapacity(newCapacity, len); + } + + /** + * Set path buffer capacity to the specified size + * + * @param capacity + * the new size + * @param len + * the amount of bytes to copy + */ + private void setPathCapacity(int capacity, int len) { + final byte[] o = path; + final byte[] n = new byte[capacity]; + System.arraycopy(o, 0, n, 0, len); + for (AbstractTreeIterator p = this; p != null && p.path == o; p = p.parent) + p.path = n; + } + + /** + * Compare the path of this current entry to another iterator's entry. + * + * @param p + * the other iterator to compare the path against. + * @return -1 if this entry sorts first; 0 if the entries are equal; 1 if + * p's entry sorts first. + */ + public int pathCompare(final AbstractTreeIterator p) { + return pathCompare(p, p.mode); + } + + int pathCompare(final AbstractTreeIterator p, final int pMode) { + final byte[] a = path; + final byte[] b = p.path; + final int aLen = pathLen; + final int bLen = p.pathLen; + int cPos; + + // Its common when we are a subtree for both parents to match; + // when this happens everything in path[0..cPos] is known to + // be equal and does not require evaluation again. + // + cPos = alreadyMatch(this, p); + + for (; cPos < aLen && cPos < bLen; cPos++) { + final int cmp = (a[cPos] & 0xff) - (b[cPos] & 0xff); + if (cmp != 0) + return cmp; + } + + if (cPos < aLen) + return (a[cPos] & 0xff) - lastPathChar(pMode); + if (cPos < bLen) + return lastPathChar(mode) - (b[cPos] & 0xff); + return lastPathChar(mode) - lastPathChar(pMode); + } + + private static int alreadyMatch(AbstractTreeIterator a, + AbstractTreeIterator b) { + for (;;) { + final AbstractTreeIterator ap = a.parent; + final AbstractTreeIterator bp = b.parent; + if (ap == null || bp == null) + return 0; + if (ap.matches == bp.matches) + return a.pathOffset; + a = ap; + b = bp; + } + } + + private static int lastPathChar(final int mode) { + return FileMode.TREE.equals(mode) ? '/' : '\0'; + } + + /** + * Check if the current entry of both iterators has the same id. + * <p> + * This method is faster than {@link #getEntryObjectId()} as it does not + * require copying the bytes out of the buffers. A direct {@link #idBuffer} + * compare operation is performed. + * + * @param otherIterator + * the other iterator to test against. + * @return true if both iterators have the same object id; false otherwise. + */ + public boolean idEqual(final AbstractTreeIterator otherIterator) { + return ObjectId.equals(idBuffer(), idOffset(), + otherIterator.idBuffer(), otherIterator.idOffset()); + } + + /** + * Get the object id of the current entry. + * + * @return an object id for the current entry. + */ + public ObjectId getEntryObjectId() { + return ObjectId.fromRaw(idBuffer(), idOffset()); + } + + /** + * Obtain the ObjectId for the current entry. + * + * @param out + * buffer to copy the object id into. + */ + public void getEntryObjectId(final MutableObjectId out) { + out.fromRaw(idBuffer(), idOffset()); + } + + /** @return the file mode of the current entry. */ + public FileMode getEntryFileMode() { + return FileMode.fromBits(mode); + } + + /** @return the file mode of the current entry as bits */ + public int getEntryRawMode() { + return mode; + } + + /** @return path of the current entry, as a string. */ + public String getEntryPathString() { + return TreeWalk.pathOf(this); + } + + /** + * Get the byte array buffer object IDs must be copied out of. + * <p> + * The id buffer contains the bytes necessary to construct an ObjectId for + * the current entry of this iterator. The buffer can be the same buffer for + * all entries, or it can be a unique buffer per-entry. Implementations are + * encouraged to expose their private buffer whenever possible to reduce + * garbage generation and copying costs. + * + * @return byte array the implementation stores object IDs within. + * @see #getEntryObjectId() + */ + public abstract byte[] idBuffer(); + + /** + * Get the position within {@link #idBuffer()} of this entry's ObjectId. + * + * @return offset into the array returned by {@link #idBuffer()} where the + * ObjectId must be copied out of. + */ + public abstract int idOffset(); + + /** + * Create a new iterator for the current entry's subtree. + * <p> + * The parent reference of the iterator must be <code>this</code>, + * otherwise the caller would not be able to exit out of the subtree + * iterator correctly and return to continue walking <code>this</code>. + * + * @param repo + * repository to load the tree data from. + * @return a new parser that walks over the current subtree. + * @throws IncorrectObjectTypeException + * the current entry is not actually a tree and cannot be parsed + * as though it were a tree. + * @throws IOException + * a loose object or pack file could not be read. + */ + public abstract AbstractTreeIterator createSubtreeIterator(Repository repo) + throws IncorrectObjectTypeException, IOException; + + /** + * Create a new iterator as though the current entry were a subtree. + * + * @return a new empty tree iterator. + */ + public EmptyTreeIterator createEmptyTreeIterator() { + return new EmptyTreeIterator(this); + } + + /** + * Create a new iterator for the current entry's subtree. + * <p> + * The parent reference of the iterator must be <code>this</code>, otherwise + * the caller would not be able to exit out of the subtree iterator + * correctly and return to continue walking <code>this</code>. + * + * @param repo + * repository to load the tree data from. + * @param idBuffer + * temporary ObjectId buffer for use by this method. + * @param curs + * window cursor to use during repository access. + * @return a new parser that walks over the current subtree. + * @throws IncorrectObjectTypeException + * the current entry is not actually a tree and cannot be parsed + * as though it were a tree. + * @throws IOException + * a loose object or pack file could not be read. + */ + public AbstractTreeIterator createSubtreeIterator(final Repository repo, + final MutableObjectId idBuffer, final WindowCursor curs) + throws IncorrectObjectTypeException, IOException { + return createSubtreeIterator(repo); + } + + /** + * Is this tree iterator positioned on its first entry? + * <p> + * An iterator is positioned on the first entry if <code>back(1)</code> + * would be an invalid request as there is no entry before the current one. + * <p> + * An empty iterator (one with no entries) will be + * <code>first() && eof()</code>. + * + * @return true if the iterator is positioned on the first entry. + */ + public abstract boolean first(); + + /** + * Is this tree iterator at its EOF point (no more entries)? + * <p> + * An iterator is at EOF if there is no current entry. + * + * @return true if we have walked all entries and have none left. + */ + public abstract boolean eof(); + + /** + * Move to next entry, populating this iterator with the entry data. + * <p> + * The delta indicates how many moves forward should occur. The most common + * delta is 1 to move to the next entry. + * <p> + * Implementations must populate the following members: + * <ul> + * <li>{@link #mode}</li> + * <li>{@link #path} (from {@link #pathOffset} to {@link #pathLen})</li> + * <li>{@link #pathLen}</li> + * </ul> + * as well as any implementation dependent information necessary to + * accurately return data from {@link #idBuffer()} and {@link #idOffset()} + * when demanded. + * + * @param delta + * number of entries to move the iterator by. Must be a positive, + * non-zero integer. + * @throws CorruptObjectException + * the tree is invalid. + */ + public abstract void next(int delta) throws CorruptObjectException; + + /** + * Move to prior entry, populating this iterator with the entry data. + * <p> + * The delta indicates how many moves backward should occur.The most common + * delta is 1 to move to the prior entry. + * <p> + * Implementations must populate the following members: + * <ul> + * <li>{@link #mode}</li> + * <li>{@link #path} (from {@link #pathOffset} to {@link #pathLen})</li> + * <li>{@link #pathLen}</li> + * </ul> + * as well as any implementation dependent information necessary to + * accurately return data from {@link #idBuffer()} and {@link #idOffset()} + * when demanded. + * + * @param delta + * number of entries to move the iterator by. Must be a positive, + * non-zero integer. + * @throws CorruptObjectException + * the tree is invalid. + */ + public abstract void back(int delta) throws CorruptObjectException; + + /** + * Advance to the next tree entry, populating this iterator with its data. + * <p> + * This method behaves like <code>seek(1)</code> but is called by + * {@link TreeWalk} only if a {@link TreeFilter} was used and ruled out the + * current entry from the results. In such cases this tree iterator may + * perform special behavior. + * + * @throws CorruptObjectException + * the tree is invalid. + */ + public void skip() throws CorruptObjectException { + next(1); + } + + /** + * Indicates to the iterator that no more entries will be read. + * <p> + * This is only invoked by TreeWalk when the iteration is aborted early due + * to a {@link org.eclipse.jgit.errors.StopWalkException} being thrown from + * within a TreeFilter. + */ + public void stopWalk() { + // Do nothing by default. Most iterators do not care. + } + + /** + * @return the length of the name component of the path for the current entry + */ + public int getNameLength() { + return pathLen - pathOffset; + } + + /** + * Get the name component of the current entry path into the provided buffer. + * + * @param buffer the buffer to get the name into, it is assumed that buffer can hold the name + * @param offset the offset of the name in the buffer + * @see #getNameLength() + */ + public void getName(byte[] buffer, int offset) { + System.arraycopy(path, pathOffset, buffer, offset, pathLen - pathOffset); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/CanonicalTreeParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/CanonicalTreeParser.java new file mode 100644 index 0000000000..6705ad992b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/CanonicalTreeParser.java @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk; + +import java.io.IOException; +import java.util.Arrays; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.MutableObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.WindowCursor; + +/** Parses raw Git trees from the canonical semi-text/semi-binary format. */ +public class CanonicalTreeParser extends AbstractTreeIterator { + private static final byte[] EMPTY = {}; + + private byte[] raw; + + /** First offset within {@link #raw} of the prior entry. */ + private int prevPtr; + + /** First offset within {@link #raw} of the current entry's data. */ + private int currPtr; + + /** Offset one past the current entry (first byte of next entry). */ + private int nextPtr; + + /** Create a new parser. */ + public CanonicalTreeParser() { + reset(EMPTY); + } + + /** + * Create a new parser for a tree appearing in a subset of a repository. + * + * @param prefix + * position of this iterator in the repository tree. The value + * may be null or the empty array to indicate the prefix is the + * root of the repository. A trailing slash ('/') is + * automatically appended if the prefix does not end in '/'. + * @param repo + * repository to load the tree data from. + * @param treeId + * identity of the tree being parsed; used only in exception + * messages if data corruption is found. + * @param curs + * a window cursor to use during data access from the repository. + * @throws MissingObjectException + * the object supplied is not available from the repository. + * @throws IncorrectObjectTypeException + * the object supplied as an argument is not actually a tree and + * cannot be parsed as though it were a tree. + * @throws IOException + * a loose object or pack file could not be read. + */ + public CanonicalTreeParser(final byte[] prefix, final Repository repo, + final AnyObjectId treeId, final WindowCursor curs) + throws IncorrectObjectTypeException, IOException { + super(prefix); + reset(repo, treeId, curs); + } + + private CanonicalTreeParser(final CanonicalTreeParser p) { + super(p); + } + + /** + * Reset this parser to walk through the given tree data. + * + * @param treeData + * the raw tree content. + */ + public void reset(final byte[] treeData) { + raw = treeData; + prevPtr = -1; + currPtr = 0; + if (!eof()) + parseEntry(); + } + + /** + * Reset this parser to walk through the given tree. + * + * @param repo + * repository to load the tree data from. + * @param id + * identity of the tree being parsed; used only in exception + * messages if data corruption is found. + * @param curs + * window cursor to use during repository access. + * @return the root level parser. + * @throws MissingObjectException + * the object supplied is not available from the repository. + * @throws IncorrectObjectTypeException + * the object supplied as an argument is not actually a tree and + * cannot be parsed as though it were a tree. + * @throws IOException + * a loose object or pack file could not be read. + */ + public CanonicalTreeParser resetRoot(final Repository repo, + final AnyObjectId id, final WindowCursor curs) + throws IncorrectObjectTypeException, IOException { + CanonicalTreeParser p = this; + while (p.parent != null) + p = (CanonicalTreeParser) p.parent; + p.reset(repo, id, curs); + return p; + } + + /** @return this iterator, or its parent, if the tree is at eof. */ + public CanonicalTreeParser next() { + CanonicalTreeParser p = this; + for (;;) { + p.next(1); + if (p.eof() && p.parent != null) { + // Parent was left pointing at the entry for us; advance + // the parent to the next entry, possibly unwinding many + // levels up the tree. + // + p = (CanonicalTreeParser) p.parent; + continue; + } + return p; + } + } + + /** + * Reset this parser to walk through the given tree. + * + * @param repo + * repository to load the tree data from. + * @param id + * identity of the tree being parsed; used only in exception + * messages if data corruption is found. + * @param curs + * window cursor to use during repository access. + * @throws MissingObjectException + * the object supplied is not available from the repository. + * @throws IncorrectObjectTypeException + * the object supplied as an argument is not actually a tree and + * cannot be parsed as though it were a tree. + * @throws IOException + * a loose object or pack file could not be read. + */ + public void reset(final Repository repo, final AnyObjectId id, + final WindowCursor curs) + throws IncorrectObjectTypeException, IOException { + final ObjectLoader ldr = repo.openObject(curs, id); + if (ldr == null) { + final ObjectId me = id.toObjectId(); + throw new MissingObjectException(me, Constants.TYPE_TREE); + } + final byte[] subtreeData = ldr.getCachedBytes(); + if (ldr.getType() != Constants.OBJ_TREE) { + final ObjectId me = id.toObjectId(); + throw new IncorrectObjectTypeException(me, Constants.TYPE_TREE); + } + reset(subtreeData); + } + + @Override + public CanonicalTreeParser createSubtreeIterator(final Repository repo, + final MutableObjectId idBuffer, final WindowCursor curs) + throws IncorrectObjectTypeException, IOException { + idBuffer.fromRaw(idBuffer(), idOffset()); + if (!FileMode.TREE.equals(mode)) { + final ObjectId me = idBuffer.toObjectId(); + throw new IncorrectObjectTypeException(me, Constants.TYPE_TREE); + } + return createSubtreeIterator0(repo, idBuffer, curs); + } + + /** + * Back door to quickly create a subtree iterator for any subtree. + * <p> + * Don't use this unless you are ObjectWalk. The method is meant to be + * called only once the current entry has been identified as a tree and its + * identity has been converted into an ObjectId. + * + * @param repo + * repository to load the tree data from. + * @param id + * ObjectId of the tree to open. + * @param curs + * window cursor to use during repository access. + * @return a new parser that walks over the current subtree. + * @throws IOException + * a loose object or pack file could not be read. + */ + public final CanonicalTreeParser createSubtreeIterator0( + final Repository repo, final AnyObjectId id, final WindowCursor curs) + throws IOException { + final CanonicalTreeParser p = new CanonicalTreeParser(this); + p.reset(repo, id, curs); + return p; + } + + public CanonicalTreeParser createSubtreeIterator(final Repository repo) + throws IncorrectObjectTypeException, IOException { + final WindowCursor curs = new WindowCursor(); + try { + return createSubtreeIterator(repo, new MutableObjectId(), curs); + } finally { + curs.release(); + } + } + + @Override + public byte[] idBuffer() { + return raw; + } + + @Override + public int idOffset() { + return nextPtr - Constants.OBJECT_ID_LENGTH; + } + + @Override + public boolean first() { + return currPtr == 0; + } + + public boolean eof() { + return currPtr == raw.length; + } + + @Override + public void next(int delta) { + if (delta == 1) { + // Moving forward one is the most common case. + // + prevPtr = currPtr; + currPtr = nextPtr; + if (!eof()) + parseEntry(); + return; + } + + // Fast skip over records, then parse the last one. + // + final int end = raw.length; + int ptr = nextPtr; + while (--delta > 0 && ptr != end) { + prevPtr = ptr; + while (raw[ptr] != 0) + ptr++; + ptr += Constants.OBJECT_ID_LENGTH + 1; + } + if (delta != 0) + throw new ArrayIndexOutOfBoundsException(delta); + currPtr = ptr; + if (!eof()) + parseEntry(); + } + + @Override + public void back(int delta) { + if (delta == 1 && 0 <= prevPtr) { + // Moving back one is common in NameTreeWalk, as the average tree + // won't have D/F type conflicts to study. + // + currPtr = prevPtr; + prevPtr = -1; + if (!eof()) + parseEntry(); + return; + } else if (delta <= 0) + throw new ArrayIndexOutOfBoundsException(delta); + + // Fast skip through the records, from the beginning of the tree. + // There is no reliable way to read the tree backwards, so we must + // parse all over again from the beginning. We hold the last "delta" + // positions in a buffer, so we can find the correct position later. + // + final int[] trace = new int[delta + 1]; + Arrays.fill(trace, -1); + int ptr = 0; + while (ptr != currPtr) { + System.arraycopy(trace, 1, trace, 0, delta); + trace[delta] = ptr; + while (raw[ptr] != 0) + ptr++; + ptr += Constants.OBJECT_ID_LENGTH + 1; + } + if (trace[1] == -1) + throw new ArrayIndexOutOfBoundsException(delta); + prevPtr = trace[0]; + currPtr = trace[1]; + parseEntry(); + } + + private void parseEntry() { + int ptr = currPtr; + byte c = raw[ptr++]; + int tmp = c - '0'; + for (;;) { + c = raw[ptr++]; + if (' ' == c) + break; + tmp <<= 3; + tmp += c - '0'; + } + mode = tmp; + + tmp = pathOffset; + for (;; tmp++) { + c = raw[ptr++]; + if (c == 0) + break; + try { + path[tmp] = c; + } catch (ArrayIndexOutOfBoundsException e) { + growPath(tmp); + path[tmp] = c; + } + } + pathLen = tmp; + nextPtr = ptr + Constants.OBJECT_ID_LENGTH; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java new file mode 100644 index 0000000000..1776b50887 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk; + +import java.io.IOException; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; + +/** Iterator over an empty tree (a directory with no files). */ +public class EmptyTreeIterator extends AbstractTreeIterator { + /** Create a new iterator with no parent. */ + public EmptyTreeIterator() { + // Create a root empty tree. + } + + EmptyTreeIterator(final AbstractTreeIterator p) { + super(p); + pathLen = pathOffset; + } + + /** + * Create an iterator for a subtree of an existing iterator. + * <p> + * The caller is responsible for setting up the path of the child iterator. + * + * @param p + * parent tree iterator. + * @param childPath + * path array to be used by the child iterator. This path must + * contain the path from the top of the walk to the first child + * and must end with a '/'. + * @param childPathOffset + * position within <code>childPath</code> where the child can + * insert its data. The value at + * <code>childPath[childPathOffset-1]</code> must be '/'. + */ + public EmptyTreeIterator(final AbstractTreeIterator p, + final byte[] childPath, final int childPathOffset) { + super(p, childPath, childPathOffset); + pathLen = childPathOffset - 1; + } + + @Override + public AbstractTreeIterator createSubtreeIterator(final Repository repo) + throws IncorrectObjectTypeException, IOException { + return new EmptyTreeIterator(this); + } + + @Override + public ObjectId getEntryObjectId() { + return ObjectId.zeroId(); + } + + @Override + public byte[] idBuffer() { + return zeroid; + } + + @Override + public int idOffset() { + return 0; + } + + @Override + public boolean first() { + return true; + } + + @Override + public boolean eof() { + return true; + } + + @Override + public void next(final int delta) throws CorruptObjectException { + // Do nothing. + } + + @Override + public void back(final int delta) throws CorruptObjectException { + // Do nothing. + } + + @Override + public void stopWalk() { + if (parent != null) + parent.stopWalk(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java new file mode 100644 index 0000000000..3ef050e7c3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * Copyright (C) 2009, Tor Arne Vestbø <torarnv@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.FS; + +/** + * Working directory iterator for standard Java IO. + * <p> + * This iterator uses the standard <code>java.io</code> package to read the + * specified working directory as part of a {@link TreeWalk}. + */ +public class FileTreeIterator extends WorkingTreeIterator { + private final File directory; + + /** + * Create a new iterator to traverse the given directory and its children. + * + * @param root + * the starting directory. This directory should correspond to + * the root of the repository. + */ + public FileTreeIterator(final File root) { + directory = root; + init(entries()); + } + + /** + * Create a new iterator to traverse a subdirectory. + * + * @param p + * the parent iterator we were created from. + * @param root + * the subdirectory. This should be a directory contained within + * the parent directory. + */ + protected FileTreeIterator(final FileTreeIterator p, final File root) { + super(p); + directory = root; + init(entries()); + } + + @Override + public AbstractTreeIterator createSubtreeIterator(final Repository repo) + throws IncorrectObjectTypeException, IOException { + return new FileTreeIterator(this, ((FileEntry) current()).file); + } + + private Entry[] entries() { + final File[] all = directory.listFiles(); + if (all == null) + return EOF; + final Entry[] r = new Entry[all.length]; + for (int i = 0; i < r.length; i++) + r[i] = new FileEntry(all[i]); + return r; + } + + /** + * Wrapper for a standard Java IO file + */ + static public class FileEntry extends Entry { + final File file; + + private final FileMode mode; + + private long length = -1; + + private long lastModified; + + FileEntry(final File f) { + file = f; + + if (f.isDirectory()) { + if (new File(f, ".git").isDirectory()) + mode = FileMode.GITLINK; + else + mode = FileMode.TREE; + } else if (FS.INSTANCE.canExecute(file)) + mode = FileMode.EXECUTABLE_FILE; + else + mode = FileMode.REGULAR_FILE; + } + + @Override + public FileMode getMode() { + return mode; + } + + @Override + public String getName() { + return file.getName(); + } + + @Override + public long getLength() { + if (length < 0) + length = file.length(); + return length; + } + + @Override + public long getLastModified() { + if (lastModified == 0) + lastModified = file.lastModified(); + return lastModified; + } + + @Override + public InputStream openInputStream() throws IOException { + return new FileInputStream(file); + } + + /** + * Get the underlying file of this entry. + * + * @return the underlying file of this entry + */ + public File getFile() { + return file; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java new file mode 100644 index 0000000000..b569174bdb --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk; + +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.Repository; + +/** + * Specialized TreeWalk to detect directory-file (D/F) name conflicts. + * <p> + * Due to the way a Git tree is organized the standard {@link TreeWalk} won't + * easily find a D/F conflict when merging two or more trees together. In the + * standard TreeWalk the file will be returned first, and then much later the + * directory will be returned. This makes it impossible for the application to + * efficiently detect and handle the conflict. + * <p> + * Using this walk implementation causes the directory to report earlier than + * usual, at the same time as the non-directory entry. This permits the + * application to handle the D/F conflict in a single step. The directory is + * returned only once, so it does not get returned later in the iteration. + * <p> + * When a D/F conflict is detected {@link TreeWalk#isSubtree()} will return true + * and {@link TreeWalk#enterSubtree()} will recurse into the subtree, no matter + * which iterator originally supplied the subtree. + * <p> + * Because conflicted directories report early, using this walk implementation + * to populate a {@link DirCacheBuilder} may cause the automatic resorting to + * run and fix the entry ordering. + * <p> + * This walk implementation requires more CPU to implement a look-ahead and a + * look-behind to merge a D/F pair together, or to skip a previously reported + * directory. In typical Git repositories the look-ahead cost is 0 and the + * look-behind doesn't trigger, as users tend not to create trees which contain + * both "foo" as a directory and "foo.c" as a file. + * <p> + * In the worst-case however several thousand look-ahead steps per walk step may + * be necessary, making the overhead quite significant. Since this worst-case + * should never happen this walk implementation has made the time/space tradeoff + * in favor of more-time/less-space, as that better suits the typical case. + */ +public class NameConflictTreeWalk extends TreeWalk { + private static final int TREE_MODE = FileMode.TREE.getBits(); + + private boolean fastMinHasMatch; + + /** + * Create a new tree walker for a given repository. + * + * @param repo + * the repository the walker will obtain data from. + */ + public NameConflictTreeWalk(final Repository repo) { + super(repo); + } + + @Override + AbstractTreeIterator min() throws CorruptObjectException { + for (;;) { + final AbstractTreeIterator minRef = fastMin(); + if (fastMinHasMatch) + return minRef; + + if (isTree(minRef)) { + if (skipEntry(minRef)) { + for (final AbstractTreeIterator t : trees) { + if (t.matches == minRef) { + t.next(1); + t.matches = null; + } + } + continue; + } + return minRef; + } + + return combineDF(minRef); + } + } + + private AbstractTreeIterator fastMin() { + fastMinHasMatch = true; + + int i = 0; + AbstractTreeIterator minRef = trees[i]; + while (minRef.eof() && ++i < trees.length) + minRef = trees[i]; + if (minRef.eof()) + return minRef; + + minRef.matches = minRef; + while (++i < trees.length) { + final AbstractTreeIterator t = trees[i]; + if (t.eof()) + continue; + + final int cmp = t.pathCompare(minRef); + if (cmp < 0) { + if (fastMinHasMatch && isTree(minRef) && !isTree(t) + && nameEqual(minRef, t)) { + // We used to be at a tree, but now we are at a file + // with the same name. Allow the file to match the + // tree anyway. + // + t.matches = minRef; + } else { + fastMinHasMatch = false; + t.matches = t; + minRef = t; + } + } else if (cmp == 0) { + // Exact name/mode match is best. + // + t.matches = minRef; + } else if (fastMinHasMatch && isTree(t) && !isTree(minRef) + && nameEqual(t, minRef)) { + // The minimum is a file (non-tree) but the next entry + // of this iterator is a tree whose name matches our file. + // This is a classic D/F conflict and commonly occurs like + // this, with no gaps in between the file and directory. + // + // Use the tree as the minimum instead (see combineDF). + // + + for (int k = 0; k < i; k++) { + final AbstractTreeIterator p = trees[k]; + if (p.matches == minRef) + p.matches = t; + } + t.matches = t; + minRef = t; + } else + fastMinHasMatch = false; + } + + return minRef; + } + + private static boolean nameEqual(final AbstractTreeIterator a, + final AbstractTreeIterator b) { + return a.pathCompare(b, TREE_MODE) == 0; + } + + private static boolean isTree(final AbstractTreeIterator p) { + return FileMode.TREE.equals(p.mode); + } + + private boolean skipEntry(final AbstractTreeIterator minRef) + throws CorruptObjectException { + // A tree D/F may have been handled earlier. We need to + // not report this path if it has already been reported. + // + for (final AbstractTreeIterator t : trees) { + if (t.matches == minRef || t.first()) + continue; + + int stepsBack = 0; + for (;;) { + stepsBack++; + t.back(1); + + final int cmp = t.pathCompare(minRef, 0); + if (cmp == 0) { + // We have already seen this "$path" before. Skip it. + // + t.next(stepsBack); + return true; + } else if (cmp < 0 || t.first()) { + // We cannot find "$path" in t; it will never appear. + // + t.next(stepsBack); + break; + } + } + } + + // We have never seen the current path before. + // + return false; + } + + private AbstractTreeIterator combineDF(final AbstractTreeIterator minRef) + throws CorruptObjectException { + // Look for a possible D/F conflict forward in the tree(s) + // as there may be a "$path/" which matches "$path". Make + // such entries match this entry. + // + AbstractTreeIterator treeMatch = null; + for (final AbstractTreeIterator t : trees) { + if (t.matches == minRef || t.eof()) + continue; + + for (;;) { + final int cmp = t.pathCompare(minRef, TREE_MODE); + if (cmp < 0) { + // The "$path/" may still appear later. + // + t.matchShift++; + t.next(1); + if (t.eof()) { + t.back(t.matchShift); + t.matchShift = 0; + break; + } + } else if (cmp == 0) { + // We have a conflict match here. + // + t.matches = minRef; + treeMatch = t; + break; + } else { + // A conflict match is not possible. + // + if (t.matchShift != 0) { + t.back(t.matchShift); + t.matchShift = 0; + } + break; + } + } + } + + if (treeMatch != null) { + // If we do have a conflict use one of the directory + // matching iterators instead of the file iterator. + // This way isSubtree is true and isRecursive works. + // + for (final AbstractTreeIterator t : trees) + if (t.matches == minRef) + t.matches = treeMatch; + return treeMatch; + } + + return minRef; + } + + @Override + void popEntriesEqual() throws CorruptObjectException { + final AbstractTreeIterator ch = currentHead; + for (int i = 0; i < trees.length; i++) { + final AbstractTreeIterator t = trees[i]; + if (t.matches == ch) { + if (t.matchShift == 0) + t.next(1); + else { + t.back(t.matchShift); + t.matchShift = 0; + } + t.matches = null; + } + } + } + + @Override + void skipEntriesEqual() throws CorruptObjectException { + final AbstractTreeIterator ch = currentHead; + for (int i = 0; i < trees.length; i++) { + final AbstractTreeIterator t = trees[i]; + if (t.matches == ch) { + if (t.matchShift == 0) + t.skip(); + else { + t.back(t.matchShift); + t.matchShift = 0; + } + t.matches = null; + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java new file mode 100644 index 0000000000..245bea8dcf --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java @@ -0,0 +1,921 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk; + +import java.io.IOException; +import java.util.Collections; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.StopWalkException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.MutableObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.WindowCursor; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.treewalk.filter.PathFilterGroup; +import org.eclipse.jgit.treewalk.filter.TreeFilter; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * Walks one or more {@link AbstractTreeIterator}s in parallel. + * <p> + * This class can perform n-way differences across as many trees as necessary. + * <p> + * Each tree added must have the same root as existing trees in the walk. + * <p> + * A TreeWalk instance can only be used once to generate results. Running a + * second time requires creating a new TreeWalk instance, or invoking + * {@link #reset()} and adding new trees before starting again. Resetting an + * existing instance may be faster for some applications as some internal + * buffers may be recycled. + * <p> + * TreeWalk instances are not thread-safe. Applications must either restrict + * usage of a TreeWalk instance to a single thread, or implement their own + * synchronization at a higher level. + * <p> + * Multiple simultaneous TreeWalk instances per {@link Repository} are + * permitted, even from concurrent threads. + */ +public class TreeWalk { + /** + * Open a tree walk and filter to exactly one path. + * <p> + * The returned tree walk is already positioned on the requested path, so + * the caller should not need to invoke {@link #next()} unless they are + * looking for a possible directory/file name conflict. + * + * @param db + * repository to read tree object data from. + * @param path + * single path to advance the tree walk instance into. + * @param trees + * one or more trees to walk through, all with the same root. + * @return a new tree walk configured for exactly this one path; null if no + * path was found in any of the trees. + * @throws IOException + * reading a pack file or loose object failed. + * @throws CorruptObjectException + * an tree object could not be read as its data stream did not + * appear to be a tree, or could not be inflated. + * @throws IncorrectObjectTypeException + * an object we expected to be a tree was not a tree. + * @throws MissingObjectException + * a tree object was not found. + */ + public static TreeWalk forPath(final Repository db, final String path, + final AnyObjectId... trees) throws MissingObjectException, + IncorrectObjectTypeException, CorruptObjectException, IOException { + final TreeWalk r = new TreeWalk(db); + r.setFilter(PathFilterGroup.createFromStrings(Collections + .singleton(path))); + r.setRecursive(r.getFilter().shouldBeRecursive()); + r.reset(trees); + return r.next() ? r : null; + } + + /** + * Open a tree walk and filter to exactly one path. + * <p> + * The returned tree walk is already positioned on the requested path, so + * the caller should not need to invoke {@link #next()} unless they are + * looking for a possible directory/file name conflict. + * + * @param db + * repository to read tree object data from. + * @param path + * single path to advance the tree walk instance into. + * @param tree + * the single tree to walk through. + * @return a new tree walk configured for exactly this one path; null if no + * path was found in any of the trees. + * @throws IOException + * reading a pack file or loose object failed. + * @throws CorruptObjectException + * an tree object could not be read as its data stream did not + * appear to be a tree, or could not be inflated. + * @throws IncorrectObjectTypeException + * an object we expected to be a tree was not a tree. + * @throws MissingObjectException + * a tree object was not found. + */ + public static TreeWalk forPath(final Repository db, final String path, + final RevTree tree) throws MissingObjectException, + IncorrectObjectTypeException, CorruptObjectException, IOException { + return forPath(db, path, new ObjectId[] { tree }); + } + + private final Repository db; + + private final MutableObjectId idBuffer = new MutableObjectId(); + + private final WindowCursor curs = new WindowCursor(); + + private TreeFilter filter; + + AbstractTreeIterator[] trees; + + private boolean recursive; + + private boolean postOrderTraversal; + + private int depth; + + private boolean advance; + + private boolean postChildren; + + AbstractTreeIterator currentHead; + + /** + * Create a new tree walker for a given repository. + * + * @param repo + * the repository the walker will obtain data from. + */ + public TreeWalk(final Repository repo) { + db = repo; + filter = TreeFilter.ALL; + trees = new AbstractTreeIterator[] { new EmptyTreeIterator() }; + } + + /** + * Get the repository this tree walker is reading from. + * + * @return the repository configured when the walker was created. + */ + public Repository getRepository() { + return db; + } + + /** + * Get the currently configured filter. + * + * @return the current filter. Never null as a filter is always needed. + */ + public TreeFilter getFilter() { + return filter; + } + + /** + * Set the tree entry filter for this walker. + * <p> + * Multiple filters may be combined by constructing an arbitrary tree of + * <code>AndTreeFilter</code> or <code>OrTreeFilter</code> instances to + * describe the boolean expression required by the application. Custom + * filter implementations may also be constructed by applications. + * <p> + * Note that filters are not thread-safe and may not be shared by concurrent + * TreeWalk instances. Every TreeWalk must be supplied its own unique + * filter, unless the filter implementation specifically states it is (and + * always will be) thread-safe. Callers may use {@link TreeFilter#clone()} + * to create a unique filter tree for this TreeWalk instance. + * + * @param newFilter + * the new filter. If null the special {@link TreeFilter#ALL} + * filter will be used instead, as it matches every entry. + * @see org.eclipse.jgit.treewalk.filter.AndTreeFilter + * @see org.eclipse.jgit.treewalk.filter.OrTreeFilter + */ + public void setFilter(final TreeFilter newFilter) { + filter = newFilter != null ? newFilter : TreeFilter.ALL; + } + + /** + * Is this walker automatically entering into subtrees? + * <p> + * If the walker is recursive then the caller will not see a subtree node + * and instead will only receive file nodes in all relevant subtrees. + * + * @return true if automatically entering subtrees is enabled. + */ + public boolean isRecursive() { + return recursive; + } + + /** + * Set the walker to enter (or not enter) subtrees automatically. + * <p> + * If recursive mode is enabled the walker will hide subtree nodes from the + * calling application and will produce only file level nodes. If a tree + * (directory) is deleted then all of the file level nodes will appear to be + * deleted, recursively, through as many levels as necessary to account for + * all entries. + * + * @param b + * true to skip subtree nodes and only obtain files nodes. + */ + public void setRecursive(final boolean b) { + recursive = b; + } + + /** + * Does this walker return a tree entry after it exits the subtree? + * <p> + * If post order traversal is enabled then the walker will return a subtree + * after it has returned the last entry within that subtree. This may cause + * a subtree to be seen by the application twice if {@link #isRecursive()} + * is false, as the application will see it once, call + * {@link #enterSubtree()}, and then see it again as it leaves the subtree. + * <p> + * If an application does not enable {@link #isRecursive()} and it does not + * call {@link #enterSubtree()} then the tree is returned only once as none + * of the children were processed. + * + * @return true if subtrees are returned after entries within the subtree. + */ + public boolean isPostOrderTraversal() { + return postOrderTraversal; + } + + /** + * Set the walker to return trees after their children. + * + * @param b + * true to get trees after their children. + * @see #isPostOrderTraversal() + */ + public void setPostOrderTraversal(final boolean b) { + postOrderTraversal = b; + } + + /** Reset this walker so new tree iterators can be added to it. */ + public void reset() { + trees = new AbstractTreeIterator[0]; + advance = false; + depth = 0; + } + + /** + * Reset this walker to run over a single existing tree. + * + * @param id + * the tree we need to parse. The walker will execute over this + * single tree if the reset is successful. + * @throws MissingObjectException + * the given tree object does not exist in this repository. + * @throws IncorrectObjectTypeException + * the given object id does not denote a tree, but instead names + * some other non-tree type of object. Note that commits are not + * trees, even if they are sometimes called a "tree-ish". + * @throws CorruptObjectException + * the object claimed to be a tree, but its contents did not + * appear to be a tree. The repository may have data corruption. + * @throws IOException + * a loose object or pack file could not be read. + */ + public void reset(final AnyObjectId id) throws MissingObjectException, + IncorrectObjectTypeException, CorruptObjectException, IOException { + if (trees.length == 1) { + AbstractTreeIterator o = trees[0]; + while (o.parent != null) + o = o.parent; + if (o instanceof CanonicalTreeParser) { + o.matches = null; + o.matchShift = 0; + ((CanonicalTreeParser) o).reset(db, id, curs); + trees[0] = o; + } else { + trees[0] = parserFor(id); + } + } else { + trees = new AbstractTreeIterator[] { parserFor(id) }; + } + + advance = false; + depth = 0; + } + + /** + * Reset this walker to run over a set of existing trees. + * + * @param ids + * the trees we need to parse. The walker will execute over this + * many parallel trees if the reset is successful. + * @throws MissingObjectException + * the given tree object does not exist in this repository. + * @throws IncorrectObjectTypeException + * the given object id does not denote a tree, but instead names + * some other non-tree type of object. Note that commits are not + * trees, even if they are sometimes called a "tree-ish". + * @throws CorruptObjectException + * the object claimed to be a tree, but its contents did not + * appear to be a tree. The repository may have data corruption. + * @throws IOException + * a loose object or pack file could not be read. + */ + public void reset(final AnyObjectId[] ids) throws MissingObjectException, + IncorrectObjectTypeException, CorruptObjectException, IOException { + final int oldLen = trees.length; + final int newLen = ids.length; + final AbstractTreeIterator[] r = newLen == oldLen ? trees + : new AbstractTreeIterator[newLen]; + for (int i = 0; i < newLen; i++) { + AbstractTreeIterator o; + + if (i < oldLen) { + o = trees[i]; + while (o.parent != null) + o = o.parent; + if (o instanceof CanonicalTreeParser && o.pathOffset == 0) { + o.matches = null; + o.matchShift = 0; + ((CanonicalTreeParser) o).reset(db, ids[i], curs); + r[i] = o; + continue; + } + } + + o = parserFor(ids[i]); + r[i] = o; + } + + trees = r; + advance = false; + depth = 0; + } + + /** + * Add an already existing tree object for walking. + * <p> + * The position of this tree is returned to the caller, in case the caller + * has lost track of the order they added the trees into the walker. + * <p> + * The tree must have the same root as existing trees in the walk. + * + * @param id + * identity of the tree object the caller wants walked. + * @return position of this tree within the walker. + * @throws MissingObjectException + * the given tree object does not exist in this repository. + * @throws IncorrectObjectTypeException + * the given object id does not denote a tree, but instead names + * some other non-tree type of object. Note that commits are not + * trees, even if they are sometimes called a "tree-ish". + * @throws CorruptObjectException + * the object claimed to be a tree, but its contents did not + * appear to be a tree. The repository may have data corruption. + * @throws IOException + * a loose object or pack file could not be read. + */ + public int addTree(final ObjectId id) throws MissingObjectException, + IncorrectObjectTypeException, CorruptObjectException, IOException { + return addTree(parserFor(id)); + } + + /** + * Add an already created tree iterator for walking. + * <p> + * The position of this tree is returned to the caller, in case the caller + * has lost track of the order they added the trees into the walker. + * <p> + * The tree which the iterator operates on must have the same root as + * existing trees in the walk. + * + * @param p + * an iterator to walk over. The iterator should be new, with no + * parent, and should still be positioned before the first entry. + * The tree which the iterator operates on must have the same root + * as other trees in the walk. + * + * @return position of this tree within the walker. + * @throws CorruptObjectException + * the iterator was unable to obtain its first entry, due to + * possible data corruption within the backing data store. + */ + public int addTree(final AbstractTreeIterator p) + throws CorruptObjectException { + final int n = trees.length; + final AbstractTreeIterator[] newTrees = new AbstractTreeIterator[n + 1]; + + System.arraycopy(trees, 0, newTrees, 0, n); + newTrees[n] = p; + p.matches = null; + p.matchShift = 0; + + trees = newTrees; + return n; + } + + /** + * Get the number of trees known to this walker. + * + * @return the total number of trees this walker is iterating over. + */ + public int getTreeCount() { + return trees.length; + } + + /** + * Advance this walker to the next relevant entry. + * + * @return true if there is an entry available; false if all entries have + * been walked and the walk of this set of tree iterators is over. + * @throws MissingObjectException + * {@link #isRecursive()} was enabled, a subtree was found, but + * the subtree object does not exist in this repository. The + * repository may be missing objects. + * @throws IncorrectObjectTypeException + * {@link #isRecursive()} was enabled, a subtree was found, and + * the subtree id does not denote a tree, but instead names some + * other non-tree type of object. The repository may have data + * corruption. + * @throws CorruptObjectException + * the contents of a tree did not appear to be a tree. The + * repository may have data corruption. + * @throws IOException + * a loose object or pack file could not be read. + */ + public boolean next() throws MissingObjectException, + IncorrectObjectTypeException, CorruptObjectException, IOException { + try { + if (advance) { + advance = false; + postChildren = false; + popEntriesEqual(); + } + + for (;;) { + final AbstractTreeIterator t = min(); + if (t.eof()) { + if (depth > 0) { + exitSubtree(); + if (postOrderTraversal) { + advance = true; + postChildren = true; + return true; + } + popEntriesEqual(); + continue; + } + return false; + } + + currentHead = t; + if (!filter.include(this)) { + skipEntriesEqual(); + continue; + } + + if (recursive && FileMode.TREE.equals(t.mode)) { + enterSubtree(); + continue; + } + + advance = true; + return true; + } + } catch (StopWalkException stop) { + for (final AbstractTreeIterator t : trees) + t.stopWalk(); + return false; + } + } + + /** + * Obtain the tree iterator for the current entry. + * <p> + * Entering into (or exiting out of) a subtree causes the current tree + * iterator instance to be changed for the nth tree. This allows the tree + * iterators to manage only one list of items, with the diving handled by + * recursive trees. + * + * @param <T> + * type of the tree iterator expected by the caller. + * @param nth + * tree to obtain the current iterator of. + * @param clazz + * type of the tree iterator expected by the caller. + * @return r the current iterator of the requested type; null if the tree + * has no entry to match the current path. + */ + public <T extends AbstractTreeIterator> T getTree(final int nth, + final Class<T> clazz) { + final AbstractTreeIterator t = trees[nth]; + return t.matches == currentHead ? (T) t : null; + } + + /** + * Obtain the raw {@link FileMode} bits for the current entry. + * <p> + * Every added tree supplies mode bits, even if the tree does not contain + * the current entry. In the latter case {@link FileMode#MISSING}'s mode + * bits (0) are returned. + * + * @param nth + * tree to obtain the mode bits from. + * @return mode bits for the current entry of the nth tree. + * @see FileMode#fromBits(int) + */ + public int getRawMode(final int nth) { + final AbstractTreeIterator t = trees[nth]; + return t.matches == currentHead ? t.mode : 0; + } + + /** + * Obtain the {@link FileMode} for the current entry. + * <p> + * Every added tree supplies a mode, even if the tree does not contain the + * current entry. In the latter case {@link FileMode#MISSING} is returned. + * + * @param nth + * tree to obtain the mode from. + * @return mode for the current entry of the nth tree. + */ + public FileMode getFileMode(final int nth) { + return FileMode.fromBits(getRawMode(nth)); + } + + /** + * Obtain the ObjectId for the current entry. + * <p> + * Using this method to compare ObjectId values between trees of this walker + * is very inefficient. Applications should try to use + * {@link #idEqual(int, int)} or {@link #getObjectId(MutableObjectId, int)} + * whenever possible. + * <p> + * Every tree supplies an object id, even if the tree does not contain the + * current entry. In the latter case {@link ObjectId#zeroId()} is returned. + * + * @param nth + * tree to obtain the object identifier from. + * @return object identifier for the current tree entry. + * @see #getObjectId(MutableObjectId, int) + * @see #idEqual(int, int) + */ + public ObjectId getObjectId(final int nth) { + final AbstractTreeIterator t = trees[nth]; + return t.matches == currentHead ? t.getEntryObjectId() : ObjectId + .zeroId(); + } + + /** + * Obtain the ObjectId for the current entry. + * <p> + * Every tree supplies an object id, even if the tree does not contain the + * current entry. In the latter case {@link ObjectId#zeroId()} is supplied. + * <p> + * Applications should try to use {@link #idEqual(int, int)} when possible + * as it avoids conversion overheads. + * + * @param out + * buffer to copy the object id into. + * @param nth + * tree to obtain the object identifier from. + * @see #idEqual(int, int) + */ + public void getObjectId(final MutableObjectId out, final int nth) { + final AbstractTreeIterator t = trees[nth]; + if (t.matches == currentHead) + t.getEntryObjectId(out); + else + out.clear(); + } + + /** + * Compare two tree's current ObjectId values for equality. + * + * @param nthA + * first tree to compare the object id from. + * @param nthB + * second tree to compare the object id from. + * @return result of + * <code>getObjectId(nthA).equals(getObjectId(nthB))</code>. + * @see #getObjectId(int) + */ + public boolean idEqual(final int nthA, final int nthB) { + final AbstractTreeIterator ch = currentHead; + final AbstractTreeIterator a = trees[nthA]; + final AbstractTreeIterator b = trees[nthB]; + if (a.matches == ch && b.matches == ch) + return a.idEqual(b); + if (a.matches != ch && b.matches != ch) { + // If neither tree matches the current path node then neither + // tree has this entry. In such case the ObjectId is zero(), + // and zero() is always equal to zero(). + // + return true; + } + return false; + } + + /** + * Get the current entry's name within its parent tree. + * <p> + * This method is not very efficient and is primarily meant for debugging + * and final output generation. Applications should try to avoid calling it, + * and if invoked do so only once per interesting entry, where the name is + * absolutely required for correct function. + * + * @return name of the current entry within the parent tree (or directory). + * The name never includes a '/'. + */ + public String getNameString() { + final AbstractTreeIterator t = currentHead; + final int off = t.pathOffset; + final int end = t.pathLen; + return RawParseUtils.decode(Constants.CHARSET, t.path, off, end); + } + + /** + * Get the current entry's complete path. + * <p> + * This method is not very efficient and is primarily meant for debugging + * and final output generation. Applications should try to avoid calling it, + * and if invoked do so only once per interesting entry, where the name is + * absolutely required for correct function. + * + * @return complete path of the current entry, from the root of the + * repository. If the current entry is in a subtree there will be at + * least one '/' in the returned string. + */ + public String getPathString() { + return pathOf(currentHead); + } + + /** + * Get the current entry's complete path as a UTF-8 byte array. + * + * @return complete path of the current entry, from the root of the + * repository. If the current entry is in a subtree there will be at + * least one '/' in the returned string. + */ + public byte[] getRawPath() { + final AbstractTreeIterator t = currentHead; + final int n = t.pathLen; + final byte[] r = new byte[n]; + System.arraycopy(t.path, 0, r, 0, n); + return r; + } + + /** + * Test if the supplied path matches the current entry's path. + * <p> + * This method tests that the supplied path is exactly equal to the current + * entry, or is one of its parent directories. It is faster to use this + * method then to use {@link #getPathString()} to first create a String + * object, then test <code>startsWith</code> or some other type of string + * match function. + * + * @param p + * path buffer to test. Callers should ensure the path does not + * end with '/' prior to invocation. + * @param pLen + * number of bytes from <code>buf</code> to test. + * @return < 0 if p is before the current path; 0 if p matches the current + * path; 1 if the current path is past p and p will never match + * again on this tree walk. + */ + public int isPathPrefix(final byte[] p, final int pLen) { + final AbstractTreeIterator t = currentHead; + final byte[] c = t.path; + final int cLen = t.pathLen; + int ci; + + for (ci = 0; ci < cLen && ci < pLen; ci++) { + final int c_value = (c[ci] & 0xff) - (p[ci] & 0xff); + if (c_value != 0) + return c_value; + } + + if (ci < cLen) { + // Ran out of pattern but we still had current data. + // If c[ci] == '/' then pattern matches the subtree. + // Otherwise we cannot be certain so we return -1. + // + return c[ci] == '/' ? 0 : -1; + } + + if (ci < pLen) { + // Ran out of current, but we still have pattern data. + // If p[ci] == '/' then pattern matches this subtree, + // otherwise we cannot be certain so we return -1. + // + return p[ci] == '/' ? 0 : -1; + } + + // Both strings are identical. + // + return 0; + } + + /** + * Test if the supplied path matches (being suffix of) the current entry's + * path. + * <p> + * This method tests that the supplied path is exactly equal to the current + * entry, or is relative to one of entry's parent directories. It is faster + * to use this method then to use {@link #getPathString()} to first create + * a String object, then test <code>endsWith</code> or some other type of + * string match function. + * + * @param p + * path buffer to test. + * @param pLen + * number of bytes from <code>buf</code> to test. + * @return true if p is suffix of the current path; + * false if otherwise + */ + public boolean isPathSuffix(final byte[] p, final int pLen) { + final AbstractTreeIterator t = currentHead; + final byte[] c = t.path; + final int cLen = t.pathLen; + int ci; + + for (ci = 1; ci < cLen && ci < pLen; ci++) { + if (c[cLen-ci] != p[pLen-ci]) + return false; + } + + return true; + } + + /** + * Get the current subtree depth of this walker. + * + * @return the current subtree depth of this walker. + */ + public int getDepth() { + return depth; + } + + /** + * Is the current entry a subtree? + * <p> + * This method is faster then testing the raw mode bits of all trees to see + * if any of them are a subtree. If at least one is a subtree then this + * method will return true. + * + * @return true if {@link #enterSubtree()} will work on the current node. + */ + public boolean isSubtree() { + return FileMode.TREE.equals(currentHead.mode); + } + + /** + * Is the current entry a subtree returned after its children? + * + * @return true if the current node is a tree that has been returned after + * its children were already processed. + * @see #isPostOrderTraversal() + */ + public boolean isPostChildren() { + return postChildren && isSubtree(); + } + + /** + * Enter into the current subtree. + * <p> + * If the current entry is a subtree this method arranges for its children + * to be returned before the next sibling following the subtree is returned. + * + * @throws MissingObjectException + * a subtree was found, but the subtree object does not exist in + * this repository. The repository may be missing objects. + * @throws IncorrectObjectTypeException + * a subtree was found, and the subtree id does not denote a + * tree, but instead names some other non-tree type of object. + * The repository may have data corruption. + * @throws CorruptObjectException + * the contents of a tree did not appear to be a tree. The + * repository may have data corruption. + * @throws IOException + * a loose object or pack file could not be read. + */ + public void enterSubtree() throws MissingObjectException, + IncorrectObjectTypeException, CorruptObjectException, IOException { + final AbstractTreeIterator ch = currentHead; + final AbstractTreeIterator[] tmp = new AbstractTreeIterator[trees.length]; + for (int i = 0; i < trees.length; i++) { + final AbstractTreeIterator t = trees[i]; + final AbstractTreeIterator n; + if (t.matches == ch && !t.eof() && FileMode.TREE.equals(t.mode)) + n = t.createSubtreeIterator(db, idBuffer, curs); + else + n = t.createEmptyTreeIterator(); + tmp[i] = n; + } + depth++; + advance = false; + System.arraycopy(tmp, 0, trees, 0, trees.length); + } + + AbstractTreeIterator min() throws CorruptObjectException { + int i = 0; + AbstractTreeIterator minRef = trees[i]; + while (minRef.eof() && ++i < trees.length) + minRef = trees[i]; + if (minRef.eof()) + return minRef; + + minRef.matches = minRef; + while (++i < trees.length) { + final AbstractTreeIterator t = trees[i]; + if (t.eof()) + continue; + final int cmp = t.pathCompare(minRef); + if (cmp < 0) { + t.matches = t; + minRef = t; + } else if (cmp == 0) { + t.matches = minRef; + } + } + + return minRef; + } + + void popEntriesEqual() throws CorruptObjectException { + final AbstractTreeIterator ch = currentHead; + for (int i = 0; i < trees.length; i++) { + final AbstractTreeIterator t = trees[i]; + if (t.matches == ch) { + t.next(1); + t.matches = null; + } + } + } + + void skipEntriesEqual() throws CorruptObjectException { + final AbstractTreeIterator ch = currentHead; + for (int i = 0; i < trees.length; i++) { + final AbstractTreeIterator t = trees[i]; + if (t.matches == ch) { + t.skip(); + t.matches = null; + } + } + } + + private void exitSubtree() { + depth--; + for (int i = 0; i < trees.length; i++) + trees[i] = trees[i].parent; + + AbstractTreeIterator minRef = null; + for (final AbstractTreeIterator t : trees) { + if (t.matches != t) + continue; + if (minRef == null || t.pathCompare(minRef) < 0) + minRef = t; + } + currentHead = minRef; + } + + private CanonicalTreeParser parserFor(final AnyObjectId id) + throws IncorrectObjectTypeException, IOException { + final CanonicalTreeParser p = new CanonicalTreeParser(); + p.reset(db, id, curs); + return p; + } + + static String pathOf(final AbstractTreeIterator t) { + return RawParseUtils.decode(Constants.CHARSET, t.path, 0, t.pathLen); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java new file mode 100644 index 0000000000..18ceef8d91 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java @@ -0,0 +1,457 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetEncoder; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Comparator; + +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; + +/** + * Walks a working directory tree as part of a {@link TreeWalk}. + * <p> + * Most applications will want to use the standard implementation of this + * iterator, {@link FileTreeIterator}, as that does all IO through the standard + * <code>java.io</code> package. Plugins for a Java based IDE may however wish + * to create their own implementations of this class to allow traversal of the + * IDE's project space, as well as benefit from any caching the IDE may have. + * + * @see FileTreeIterator + */ +public abstract class WorkingTreeIterator extends AbstractTreeIterator { + /** An empty entry array, suitable for {@link #init(Entry[])}. */ + protected static final Entry[] EOF = {}; + + /** Size we perform file IO in if we have to read and hash a file. */ + private static final int BUFFER_SIZE = 2048; + + /** The {@link #idBuffer()} for the current entry. */ + private byte[] contentId; + + /** Index within {@link #entries} that {@link #contentId} came from. */ + private int contentIdFromPtr; + + /** Buffer used to perform {@link #contentId} computations. */ + private byte[] contentReadBuffer; + + /** Digest computer for {@link #contentId} computations. */ + private MessageDigest contentDigest; + + /** File name character encoder. */ + private final CharsetEncoder nameEncoder; + + /** List of entries obtained from the subclass. */ + private Entry[] entries; + + /** Total number of entries in {@link #entries} that are valid. */ + private int entryCnt; + + /** Current position within {@link #entries}. */ + private int ptr; + + /** Create a new iterator with no parent. */ + protected WorkingTreeIterator() { + super(); + nameEncoder = Constants.CHARSET.newEncoder(); + } + + /** + * Create a new iterator with no parent and a prefix. + * <p> + * The prefix path supplied is inserted in front of all paths generated by + * this iterator. It is intended to be used when an iterator is being + * created for a subsection of an overall repository and needs to be + * combined with other iterators that are created to run over the entire + * repository namespace. + * + * @param prefix + * position of this iterator in the repository tree. The value + * may be null or the empty string to indicate the prefix is the + * root of the repository. A trailing slash ('/') is + * automatically appended if the prefix does not end in '/'. + */ + protected WorkingTreeIterator(final String prefix) { + super(prefix); + nameEncoder = Constants.CHARSET.newEncoder(); + } + + /** + * Create an iterator for a subtree of an existing iterator. + * + * @param p + * parent tree iterator. + */ + protected WorkingTreeIterator(final WorkingTreeIterator p) { + super(p); + nameEncoder = p.nameEncoder; + } + + @Override + public byte[] idBuffer() { + if (contentIdFromPtr == ptr) + return contentId; + switch (mode & FileMode.TYPE_MASK) { + case FileMode.TYPE_FILE: + contentIdFromPtr = ptr; + return contentId = idBufferBlob(entries[ptr]); + case FileMode.TYPE_SYMLINK: + // Java does not support symbolic links, so we should not + // have reached this particular part of the walk code. + // + return zeroid; + case FileMode.TYPE_GITLINK: + // TODO: Support obtaining current HEAD SHA-1 from nested repository + // + return zeroid; + } + return zeroid; + } + + private void initializeDigest() { + if (contentDigest != null) + return; + + if (parent == null) { + contentReadBuffer = new byte[BUFFER_SIZE]; + contentDigest = Constants.newMessageDigest(); + } else { + final WorkingTreeIterator p = (WorkingTreeIterator) parent; + p.initializeDigest(); + contentReadBuffer = p.contentReadBuffer; + contentDigest = p.contentDigest; + } + } + + private static final byte[] digits = { '0', '1', '2', '3', '4', '5', '6', + '7', '8', '9' }; + + private static final byte[] hblob = Constants + .encodedTypeString(Constants.OBJ_BLOB); + + private byte[] idBufferBlob(final Entry e) { + try { + final InputStream is = e.openInputStream(); + if (is == null) + return zeroid; + try { + initializeDigest(); + + contentDigest.reset(); + contentDigest.update(hblob); + contentDigest.update((byte) ' '); + + final long blobLength = e.getLength(); + long sz = blobLength; + if (sz == 0) { + contentDigest.update((byte) '0'); + } else { + final int bufn = contentReadBuffer.length; + int p = bufn; + do { + contentReadBuffer[--p] = digits[(int) (sz % 10)]; + sz /= 10; + } while (sz > 0); + contentDigest.update(contentReadBuffer, p, bufn - p); + } + contentDigest.update((byte) 0); + + for (;;) { + final int r = is.read(contentReadBuffer); + if (r <= 0) + break; + contentDigest.update(contentReadBuffer, 0, r); + sz += r; + } + if (sz != blobLength) + return zeroid; + return contentDigest.digest(); + } finally { + try { + is.close(); + } catch (IOException err2) { + // Suppress any error related to closing an input + // stream. We don't care, we should not have any + // outstanding data to flush or anything like that. + } + } + } catch (IOException err) { + // Can't read the file? Don't report the failure either. + // + return zeroid; + } + } + + @Override + public int idOffset() { + return 0; + } + + @Override + public boolean first() { + return ptr == 0; + } + + @Override + public boolean eof() { + return ptr == entryCnt; + } + + @Override + public void next(final int delta) throws CorruptObjectException { + ptr += delta; + if (!eof()) + parseEntry(); + } + + @Override + public void back(final int delta) throws CorruptObjectException { + ptr -= delta; + parseEntry(); + } + + private void parseEntry() { + final Entry e = entries[ptr]; + mode = e.getMode().getBits(); + + final int nameLen = e.encodedNameLen; + ensurePathCapacity(pathOffset + nameLen, pathOffset); + System.arraycopy(e.encodedName, 0, path, pathOffset, nameLen); + pathLen = pathOffset + nameLen; + } + + /** + * Get the byte length of this entry. + * + * @return size of this file, in bytes. + */ + public long getEntryLength() { + return current().getLength(); + } + + /** + * Get the last modified time of this entry. + * + * @return last modified time of this file, in milliseconds since the epoch + * (Jan 1, 1970 UTC). + */ + public long getEntryLastModified() { + return current().getLastModified(); + } + + private static final Comparator<Entry> ENTRY_CMP = new Comparator<Entry>() { + public int compare(final Entry o1, final Entry o2) { + final byte[] a = o1.encodedName; + final byte[] b = o2.encodedName; + final int aLen = o1.encodedNameLen; + final int bLen = o2.encodedNameLen; + int cPos; + + for (cPos = 0; cPos < aLen && cPos < bLen; cPos++) { + final int cmp = (a[cPos] & 0xff) - (b[cPos] & 0xff); + if (cmp != 0) + return cmp; + } + + if (cPos < aLen) + return (a[cPos] & 0xff) - lastPathChar(o2); + if (cPos < bLen) + return lastPathChar(o1) - (b[cPos] & 0xff); + return lastPathChar(o1) - lastPathChar(o2); + } + }; + + static int lastPathChar(final Entry e) { + return e.getMode() == FileMode.TREE ? '/' : '\0'; + } + + /** + * Constructor helper. + * + * @param list + * files in the subtree of the work tree this iterator operates + * on + */ + protected void init(final Entry[] list) { + // Filter out nulls, . and .. as these are not valid tree entries, + // also cache the encoded forms of the path names for efficient use + // later on during sorting and iteration. + // + entries = list; + int i, o; + + for (i = 0, o = 0; i < entries.length; i++) { + final Entry e = entries[i]; + if (e == null) + continue; + final String name = e.getName(); + if (".".equals(name) || "..".equals(name)) + continue; + if (".git".equals(name)) + continue; + if (i != o) + entries[o] = e; + e.encodeName(nameEncoder); + o++; + } + entryCnt = o; + Arrays.sort(entries, 0, entryCnt, ENTRY_CMP); + + contentIdFromPtr = -1; + ptr = 0; + if (!eof()) + parseEntry(); + } + + /** + * Obtain the current entry from this iterator. + * + * @return the currently selected entry. + */ + protected Entry current() { + return entries[ptr]; + } + + /** A single entry within a working directory tree. */ + protected static abstract class Entry { + byte[] encodedName; + + int encodedNameLen; + + void encodeName(final CharsetEncoder enc) { + final ByteBuffer b; + try { + b = enc.encode(CharBuffer.wrap(getName())); + } catch (CharacterCodingException e) { + // This should so never happen. + throw new RuntimeException("Unencodeable file: " + getName()); + } + + encodedNameLen = b.limit(); + if (b.hasArray() && b.arrayOffset() == 0) + encodedName = b.array(); + else + b.get(encodedName = new byte[encodedNameLen]); + } + + public String toString() { + return getMode().toString() + " " + getName(); + } + + /** + * Get the type of this entry. + * <p> + * <b>Note: Efficient implementation required.</b> + * <p> + * The implementation of this method must be efficient. If a subclass + * needs to compute the value they should cache the reference within an + * instance member instead. + * + * @return a file mode constant from {@link FileMode}. + */ + public abstract FileMode getMode(); + + /** + * Get the byte length of this entry. + * <p> + * <b>Note: Efficient implementation required.</b> + * <p> + * The implementation of this method must be efficient. If a subclass + * needs to compute the value they should cache the reference within an + * instance member instead. + * + * @return size of this file, in bytes. + */ + public abstract long getLength(); + + /** + * Get the last modified time of this entry. + * <p> + * <b>Note: Efficient implementation required.</b> + * <p> + * The implementation of this method must be efficient. If a subclass + * needs to compute the value they should cache the reference within an + * instance member instead. + * + * @return time since the epoch (in ms) of the last change. + */ + public abstract long getLastModified(); + + /** + * Get the name of this entry within its directory. + * <p> + * Efficient implementations are not required. The caller will obtain + * the name only once and cache it once obtained. + * + * @return name of the entry. + */ + public abstract String getName(); + + /** + * Obtain an input stream to read the file content. + * <p> + * Efficient implementations are not required. The caller will usually + * obtain the stream only once per entry, if at all. + * <p> + * The input stream should not use buffering if the implementation can + * avoid it. The caller will buffer as necessary to perform efficient + * block IO operations. + * <p> + * The caller will close the stream once complete. + * + * @return a stream to read from the file. + * @throws IOException + * the file could not be opened for reading. + */ + public abstract InputStream openInputStream() throws IOException; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/AndTreeFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/AndTreeFilter.java new file mode 100644 index 0000000000..2c3983b015 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/AndTreeFilter.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk.filter; + +import java.io.IOException; +import java.util.Collection; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.treewalk.TreeWalk; + +/** + * Includes a tree entry only if all subfilters include the same tree entry. + * <p> + * Classic shortcut behavior is used, so evaluation of the + * {@link TreeFilter#include(TreeWalk)} method stops as soon as a false result + * is obtained. Applications can improve filtering performance by placing faster + * filters that are more likely to reject a result earlier in the list. + */ +public abstract class AndTreeFilter extends TreeFilter { + /** + * Create a filter with two filters, both of which must match. + * + * @param a + * first filter to test. + * @param b + * second filter to test. + * @return a filter that must match both input filters. + */ + public static TreeFilter create(final TreeFilter a, final TreeFilter b) { + if (a == ALL) + return b; + if (b == ALL) + return a; + return new Binary(a, b); + } + + /** + * Create a filter around many filters, all of which must match. + * + * @param list + * list of filters to match against. Must contain at least 2 + * filters. + * @return a filter that must match all input filters. + */ + public static TreeFilter create(final TreeFilter[] list) { + if (list.length == 2) + return create(list[0], list[1]); + if (list.length < 2) + throw new IllegalArgumentException("At least two filters needed."); + final TreeFilter[] subfilters = new TreeFilter[list.length]; + System.arraycopy(list, 0, subfilters, 0, list.length); + return new List(subfilters); + } + + /** + * Create a filter around many filters, all of which must match. + * + * @param list + * list of filters to match against. Must contain at least 2 + * filters. + * @return a filter that must match all input filters. + */ + public static TreeFilter create(final Collection<TreeFilter> list) { + if (list.size() < 2) + throw new IllegalArgumentException("At least two filters needed."); + final TreeFilter[] subfilters = new TreeFilter[list.size()]; + list.toArray(subfilters); + if (subfilters.length == 2) + return create(subfilters[0], subfilters[1]); + return new List(subfilters); + } + + private static class Binary extends AndTreeFilter { + private final TreeFilter a; + + private final TreeFilter b; + + Binary(final TreeFilter one, final TreeFilter two) { + a = one; + b = two; + } + + @Override + public boolean include(final TreeWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + return a.include(walker) && b.include(walker); + } + + @Override + public boolean shouldBeRecursive() { + return a.shouldBeRecursive() || b.shouldBeRecursive(); + } + + @Override + public TreeFilter clone() { + return new Binary(a.clone(), b.clone()); + } + + @Override + public String toString() { + return "(" + a.toString() + " AND " + b.toString() + ")"; + } + } + + private static class List extends AndTreeFilter { + private final TreeFilter[] subfilters; + + List(final TreeFilter[] list) { + subfilters = list; + } + + @Override + public boolean include(final TreeWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + for (final TreeFilter f : subfilters) { + if (!f.include(walker)) + return false; + } + return true; + } + + @Override + public boolean shouldBeRecursive() { + for (final TreeFilter f : subfilters) + if (f.shouldBeRecursive()) + return true; + return false; + } + + @Override + public TreeFilter clone() { + final TreeFilter[] s = new TreeFilter[subfilters.length]; + for (int i = 0; i < s.length; i++) + s[i] = subfilters[i].clone(); + return new List(s); + } + + @Override + public String toString() { + final StringBuffer r = new StringBuffer(); + r.append("("); + for (int i = 0; i < subfilters.length; i++) { + if (i > 0) + r.append(" AND "); + r.append(subfilters[i].toString()); + } + r.append(")"); + return r.toString(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/NotTreeFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/NotTreeFilter.java new file mode 100644 index 0000000000..33e4415a97 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/NotTreeFilter.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk.filter; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.treewalk.TreeWalk; + +/** Includes an entry only if the subfilter does not include the entry. */ +public class NotTreeFilter extends TreeFilter { + /** + * Create a filter that negates the result of another filter. + * + * @param a + * filter to negate. + * @return a filter that does the reverse of <code>a</code>. + */ + public static TreeFilter create(final TreeFilter a) { + return new NotTreeFilter(a); + } + + private final TreeFilter a; + + private NotTreeFilter(final TreeFilter one) { + a = one; + } + + @Override + public TreeFilter negate() { + return a; + } + + @Override + public boolean include(final TreeWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + return !a.include(walker); + } + + @Override + public boolean shouldBeRecursive() { + return a.shouldBeRecursive(); + } + + @Override + public TreeFilter clone() { + final TreeFilter n = a.clone(); + return n == a ? this : new NotTreeFilter(n); + } + + @Override + public String toString() { + return "NOT " + a.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/OrTreeFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/OrTreeFilter.java new file mode 100644 index 0000000000..55005446e5 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/OrTreeFilter.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk.filter; + +import java.io.IOException; +import java.util.Collection; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.treewalk.TreeWalk; + +/** + * Includes a tree entry if any subfilters include the same tree entry. + * <p> + * Classic shortcut behavior is used, so evaluation of the + * {@link TreeFilter#include(TreeWalk)} method stops as soon as a true result is + * obtained. Applications can improve filtering performance by placing faster + * filters that are more likely to accept a result earlier in the list. + */ +public abstract class OrTreeFilter extends TreeFilter { + /** + * Create a filter with two filters, one of which must match. + * + * @param a + * first filter to test. + * @param b + * second filter to test. + * @return a filter that must match at least one input filter. + */ + public static TreeFilter create(final TreeFilter a, final TreeFilter b) { + if (a == ALL || b == ALL) + return ALL; + return new Binary(a, b); + } + + /** + * Create a filter around many filters, one of which must match. + * + * @param list + * list of filters to match against. Must contain at least 2 + * filters. + * @return a filter that must match at least one input filter. + */ + public static TreeFilter create(final TreeFilter[] list) { + if (list.length == 2) + return create(list[0], list[1]); + if (list.length < 2) + throw new IllegalArgumentException("At least two filters needed."); + final TreeFilter[] subfilters = new TreeFilter[list.length]; + System.arraycopy(list, 0, subfilters, 0, list.length); + return new List(subfilters); + } + + /** + * Create a filter around many filters, one of which must match. + * + * @param list + * list of filters to match against. Must contain at least 2 + * filters. + * @return a filter that must match at least one input filter. + */ + public static TreeFilter create(final Collection<TreeFilter> list) { + if (list.size() < 2) + throw new IllegalArgumentException("At least two filters needed."); + final TreeFilter[] subfilters = new TreeFilter[list.size()]; + list.toArray(subfilters); + if (subfilters.length == 2) + return create(subfilters[0], subfilters[1]); + return new List(subfilters); + } + + private static class Binary extends OrTreeFilter { + private final TreeFilter a; + + private final TreeFilter b; + + Binary(final TreeFilter one, final TreeFilter two) { + a = one; + b = two; + } + + @Override + public boolean include(final TreeWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + return a.include(walker) || b.include(walker); + } + + @Override + public boolean shouldBeRecursive() { + return a.shouldBeRecursive() || b.shouldBeRecursive(); + } + + @Override + public TreeFilter clone() { + return new Binary(a.clone(), b.clone()); + } + + @Override + public String toString() { + return "(" + a.toString() + " OR " + b.toString() + ")"; + } + } + + private static class List extends OrTreeFilter { + private final TreeFilter[] subfilters; + + List(final TreeFilter[] list) { + subfilters = list; + } + + @Override + public boolean include(final TreeWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + for (final TreeFilter f : subfilters) { + if (f.include(walker)) + return true; + } + return false; + } + + @Override + public boolean shouldBeRecursive() { + for (final TreeFilter f : subfilters) + if (f.shouldBeRecursive()) + return true; + return false; + } + + @Override + public TreeFilter clone() { + final TreeFilter[] s = new TreeFilter[subfilters.length]; + for (int i = 0; i < s.length; i++) + s[i] = subfilters[i].clone(); + return new List(s); + } + + @Override + public String toString() { + final StringBuffer r = new StringBuffer(); + r.append("("); + for (int i = 0; i < subfilters.length; i++) { + if (i > 0) + r.append(" OR "); + r.append(subfilters[i].toString()); + } + r.append(")"); + return r.toString(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilter.java new file mode 100644 index 0000000000..5883d655ef --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilter.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk.filter; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.treewalk.TreeWalk; + +/** + * Includes tree entries only if they match the configured path. + * <p> + * Applications should use {@link PathFilterGroup} to connect these into a tree + * filter graph, as the group supports breaking out of traversal once it is + * known the path can never match. + */ +public class PathFilter extends TreeFilter { + /** + * Create a new tree filter for a user supplied path. + * <p> + * Path strings are relative to the root of the repository. If the user's + * input should be assumed relative to a subdirectory of the repository the + * caller must prepend the subdirectory's path prior to creating the filter. + * <p> + * Path strings use '/' to delimit directories on all platforms. + * + * @param path + * the path to filter on. Must not be the empty string. All + * trailing '/' characters will be trimmed before string's length + * is checked or is used as part of the constructed filter. + * @return a new filter for the requested path. + * @throws IllegalArgumentException + * the path supplied was the empty string. + */ + public static PathFilter create(String path) { + while (path.endsWith("/")) + path = path.substring(0, path.length() - 1); + if (path.length() == 0) + throw new IllegalArgumentException("Empty path not permitted."); + return new PathFilter(path); + } + + final String pathStr; + + final byte[] pathRaw; + + private PathFilter(final String s) { + pathStr = s; + pathRaw = Constants.encode(pathStr); + } + + @Override + public boolean include(final TreeWalk walker) { + return walker.isPathPrefix(pathRaw, pathRaw.length) == 0; + } + + @Override + public boolean shouldBeRecursive() { + for (final byte b : pathRaw) + if (b == '/') + return true; + return false; + } + + @Override + public TreeFilter clone() { + return this; + } + + public String toString() { + return "PATH(\"" + pathStr + "\")"; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java new file mode 100644 index 0000000000..cd11f8123b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk.filter; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; + +import org.eclipse.jgit.errors.StopWalkException; +import org.eclipse.jgit.treewalk.TreeWalk; + +/** + * Includes tree entries only if they match one or more configured paths. + * <p> + * Operates like {@link PathFilter} but causes the walk to abort as soon as the + * tree can no longer match any of the paths within the group. This may bypass + * the boolean logic of a higher level AND or OR group, but does improve + * performance for the common case of examining one or more modified paths. + * <p> + * This filter is effectively an OR group around paths, with the early abort + * feature described above. + */ +public class PathFilterGroup { + /** + * Create a collection of path filters from Java strings. + * <p> + * Path strings are relative to the root of the repository. If the user's + * input should be assumed relative to a subdirectory of the repository the + * caller must prepend the subdirectory's path prior to creating the filter. + * <p> + * Path strings use '/' to delimit directories on all platforms. + * <p> + * Paths may appear in any order within the collection. Sorting may be done + * internally when the group is constructed if doing so will improve path + * matching performance. + * + * @param paths + * the paths to test against. Must have at least one entry. + * @return a new filter for the list of paths supplied. + */ + public static TreeFilter createFromStrings(final Collection<String> paths) { + if (paths.isEmpty()) + throw new IllegalArgumentException("At least one path is required."); + final PathFilter[] p = new PathFilter[paths.size()]; + int i = 0; + for (final String s : paths) + p[i++] = PathFilter.create(s); + return create(p); + } + + /** + * Create a collection of path filters. + * <p> + * Paths may appear in any order within the collection. Sorting may be done + * internally when the group is constructed if doing so will improve path + * matching performance. + * + * @param paths + * the paths to test against. Must have at least one entry. + * @return a new filter for the list of paths supplied. + */ + public static TreeFilter create(final Collection<PathFilter> paths) { + if (paths.isEmpty()) + throw new IllegalArgumentException("At least one path is required."); + final PathFilter[] p = new PathFilter[paths.size()]; + paths.toArray(p); + return create(p); + } + + private static TreeFilter create(final PathFilter[] p) { + if (p.length == 1) + return new Single(p[0]); + return new Group(p); + } + + static class Single extends TreeFilter { + private final PathFilter path; + + private final byte[] raw; + + private Single(final PathFilter p) { + path = p; + raw = path.pathRaw; + } + + @Override + public boolean include(final TreeWalk walker) { + final int cmp = walker.isPathPrefix(raw, raw.length); + if (cmp > 0) + throw StopWalkException.INSTANCE; + return cmp == 0; + } + + @Override + public boolean shouldBeRecursive() { + return path.shouldBeRecursive(); + } + + @Override + public TreeFilter clone() { + return this; + } + + public String toString() { + return "FAST_" + path.toString(); + } + } + + static class Group extends TreeFilter { + private static final Comparator<PathFilter> PATH_SORT = new Comparator<PathFilter>() { + public int compare(final PathFilter o1, final PathFilter o2) { + return o1.pathStr.compareTo(o2.pathStr); + } + }; + + private final PathFilter[] paths; + + private Group(final PathFilter[] p) { + paths = p; + Arrays.sort(paths, PATH_SORT); + } + + @Override + public boolean include(final TreeWalk walker) { + final int n = paths.length; + for (int i = 0;;) { + final byte[] r = paths[i].pathRaw; + final int cmp = walker.isPathPrefix(r, r.length); + if (cmp == 0) + return true; + if (++i < n) + continue; + if (cmp > 0) + throw StopWalkException.INSTANCE; + return false; + } + } + + @Override + public boolean shouldBeRecursive() { + for (final PathFilter p : paths) + if (p.shouldBeRecursive()) + return true; + return false; + } + + @Override + public TreeFilter clone() { + return this; + } + + public String toString() { + final StringBuffer r = new StringBuffer(); + r.append("FAST("); + for (int i = 0; i < paths.length; i++) { + if (i > 0) + r.append(" OR "); + r.append(paths[i].toString()); + } + r.append(")"); + return r.toString(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathSuffixFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathSuffixFilter.java new file mode 100644 index 0000000000..3721ec646f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathSuffixFilter.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk.filter; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.treewalk.TreeWalk; + +/** + * Includes tree entries only if they match the configured path. + */ +public class PathSuffixFilter extends TreeFilter { + + /** + * Create a new tree filter for a user supplied path. + * <p> + * Path strings use '/' to delimit directories on all platforms. + * + * @param path + * the path (suffix) to filter on. Must not be the empty string. + * @return a new filter for the requested path. + * @throws IllegalArgumentException + * the path supplied was the empty string. + */ + public static PathSuffixFilter create(String path) { + if (path.length() == 0) + throw new IllegalArgumentException("Empty path not permitted."); + return new PathSuffixFilter(path); + } + + final String pathStr; + final byte[] pathRaw; + + private PathSuffixFilter(final String s) { + pathStr = s; + pathRaw = Constants.encode(pathStr); + } + + @Override + public TreeFilter clone() { + return this; + } + + @Override + public boolean include(TreeWalk walker) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + if (walker.isSubtree()) + return true; + else + return walker.isPathSuffix(pathRaw, pathRaw.length); + + } + + @Override + public boolean shouldBeRecursive() { + return true; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/TreeFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/TreeFilter.java new file mode 100644 index 0000000000..5d0fb12f51 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/TreeFilter.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.treewalk.filter; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.treewalk.TreeWalk; + +/** + * Selects interesting tree entries during walking. + * <p> + * This is an abstract interface. Applications may implement a subclass, or use + * one of the predefined implementations already available within this package. + * <p> + * Unless specifically noted otherwise a TreeFilter implementation is not thread + * safe and may not be shared by different TreeWalk instances at the same time. + * This restriction allows TreeFilter implementations to cache state within + * their instances during {@link #include(TreeWalk)} if it is beneficial to + * their implementation. Deep clones created by {@link #clone()} may be used to + * construct a thread-safe copy of an existing filter. + * + * <p> + * <b>Path filters:</b> + * <ul> + * <li>Matching pathname: {@link PathFilter}</li> + * </ul> + * + * <p> + * <b>Difference filters:</b> + * <ul> + * <li>Only select differences: {@link #ANY_DIFF}.</li> + * </ul> + * + * <p> + * <b>Boolean modifiers:</b> + * <ul> + * <li>AND: {@link AndTreeFilter}</li> + * <li>OR: {@link OrTreeFilter}</li> + * <li>NOT: {@link NotTreeFilter}</li> + * </ul> + */ +public abstract class TreeFilter { + /** Selects all tree entries. */ + public static final TreeFilter ALL = new TreeFilter() { + @Override + public boolean include(final TreeWalk walker) { + return true; + } + + @Override + public boolean shouldBeRecursive() { + return false; + } + + @Override + public TreeFilter clone() { + return this; + } + + @Override + public String toString() { + return "ALL"; + } + }; + + /** + * Selects only tree entries which differ between at least 2 trees. + * <p> + * This filter also prevents a TreeWalk from recursing into a subtree if all + * parent trees have the identical subtree at the same path. This + * dramatically improves walk performance as only the changed subtrees are + * entered into. + * <p> + * If this filter is applied to a walker with only one tree it behaves like + * {@link #ALL}, or as though the walker was matching a virtual empty tree + * against the single tree it was actually given. Applications may wish to + * treat such a difference as "all names added". + */ + public static final TreeFilter ANY_DIFF = new TreeFilter() { + private static final int baseTree = 0; + + @Override + public boolean include(final TreeWalk walker) { + final int n = walker.getTreeCount(); + if (n == 1) // Assume they meant difference to empty tree. + return true; + + final int m = walker.getRawMode(baseTree); + for (int i = 1; i < n; i++) + if (walker.getRawMode(i) != m || !walker.idEqual(i, baseTree)) + return true; + return false; + } + + @Override + public boolean shouldBeRecursive() { + return false; + } + + @Override + public TreeFilter clone() { + return this; + } + + @Override + public String toString() { + return "ANY_DIFF"; + } + }; + + /** + * Create a new filter that does the opposite of this filter. + * + * @return a new filter that includes tree entries this filter rejects. + */ + public TreeFilter negate() { + return NotTreeFilter.create(this); + } + + /** + * Determine if the current entry is interesting to report. + * <p> + * This method is consulted for subtree entries even if + * {@link TreeWalk#isRecursive()} is enabled. The consultation allows the + * filter to bypass subtree recursion on a case-by-case basis, even when + * recursion is enabled at the application level. + * + * @param walker + * the walker the filter needs to examine. + * @return true if the current entry should be seen by the application; + * false to hide the entry. + * @throws MissingObjectException + * an object the filter needs to consult to determine its answer + * does not exist in the Git repository the walker is operating + * on. Filtering this current walker entry is impossible without + * the object. + * @throws IncorrectObjectTypeException + * an object the filter needed to consult was not of the + * expected object type. This usually indicates a corrupt + * repository, as an object link is referencing the wrong type. + * @throws IOException + * a loose object or pack file could not be read to obtain data + * necessary for the filter to make its decision. + */ + public abstract boolean include(TreeWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, + IOException; + + /** + * Does this tree filter require a recursive walk to match everything? + * <p> + * If this tree filter is matching on full entry path names and its pattern + * is looking for a '/' then the filter would require a recursive TreeWalk + * to accurately make its decisions. The walker is not required to enable + * recursive behavior for any particular filter, this is only a hint. + * + * @return true if the filter would like to have the walker recurse into + * subtrees to make sure it matches everything correctly; false if + * the filter does not require entering subtrees. + */ + public abstract boolean shouldBeRecursive(); + + /** + * Clone this tree filter, including its parameters. + * <p> + * This is a deep clone. If this filter embeds objects or other filters it + * must also clone those, to ensure the instances do not share mutable data. + * + * @return another copy of this filter, suitable for another thread. + */ + public abstract TreeFilter clone(); + + @Override + public String toString() { + String n = getClass().getName(); + int lastDot = n.lastIndexOf('.'); + if (lastDot >= 0) { + n = n.substring(lastDot + 1); + } + return n.replace('$', '.'); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Base64.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Base64.java new file mode 100644 index 0000000000..53c7beced8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Base64.java @@ -0,0 +1,1475 @@ +// +// NOTE: The following source code is the iHarder.net public domain +// Base64 library and is provided here as a convenience. For updates, +// problems, questions, etc. regarding this code, please visit: +// http://iharder.sourceforge.net/current/java/base64/ +// + +package org.eclipse.jgit.util; + +import java.io.Closeable; +import java.io.IOException; + + +/** + * Encodes and decodes to and from Base64 notation. + * + * <p> + * Change Log: + * </p> + * <ul> + * <li>v2.1 - Cleaned up javadoc comments and unused variables and methods. Added + * some convenience methods for reading and writing to and from files.</li> + * <li>v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems + * with other encodings (like EBCDIC).</li> + * <li>v2.0.1 - Fixed an error when decoding a single byte, that is, when the + * encoded data was a single byte.</li> + * <li>v2.0 - I got rid of methods that used booleans to set options. + * Now everything is more consolidated and cleaner. The code now detects + * when data that's being decoded is gzip-compressed and will decompress it + * automatically. Generally things are cleaner. You'll probably have to + * change some method calls that you were making to support the new + * options format (<tt>int</tt>s that you "OR" together).</li> + * <li>v1.5.1 - Fixed bug when decompressing and decoding to a + * byte[] using <tt>decode( String s, boolean gzipCompressed )</tt>. + * Added the ability to "suspend" encoding in the Output Stream so + * you can turn on and off the encoding if you need to embed base64 + * data in an otherwise "normal" stream (like an XML file).</li> + * <li>v1.5 - Output stream passes on flush() command but doesn't do anything itself. + * This helps when using GZIP streams. + * Added the ability to GZip-compress objects before encoding them.</li> + * <li>v1.4 - Added helper methods to read/write files.</li> + * <li>v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.</li> + * <li>v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream + * where last buffer being read, if not completely full, was not returned.</li> + * <li>v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.</li> + * <li>v1.3.3 - Fixed I/O streams which were totally messed up.</li> + * </ul> + * + * <p> + * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit <a href="http://iharder.net/base64">http://iharder.net/base64</a> + * periodically to check for updates or to contribute improvements. + * </p> + * + * @author Robert Harder + * @author rob@iharder.net + * @version 2.1 + */ +public class Base64 +{ + +/* ******** P U B L I C F I E L D S ******** */ + + + /** No options specified. Value is zero. */ + public final static int NO_OPTIONS = 0; + + /** Specify encoding. */ + public final static int ENCODE = 1; + + + /** Specify decoding. */ + public final static int DECODE = 0; + + + /** Specify that data should be gzip-compressed. */ + public final static int GZIP = 2; + + + /** Don't break lines when encoding (violates strict Base64 specification) */ + public final static int DONT_BREAK_LINES = 8; + + +/* ******** P R I V A T E F I E L D S ******** */ + + + /** Maximum line length (76) of Base64 output. */ + private final static int MAX_LINE_LENGTH = 76; + + + /** The equals sign (=) as a byte. */ + private final static byte EQUALS_SIGN = (byte)'='; + + + /** The new line character (\n) as a byte. */ + private final static byte NEW_LINE = (byte)'\n'; + + + /** Preferred encoding. */ + private final static String PREFERRED_ENCODING = "UTF-8"; + + + /** The 64 valid Base64 values. */ + private final static byte[] ALPHABET; + private final static byte[] _NATIVE_ALPHABET = /* May be something funny like EBCDIC */ + { + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', + (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', + (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', + (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', + (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', + (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/' + }; + + /** Determine which ALPHABET to use. */ + static + { + byte[] __bytes; + try + { + __bytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".getBytes( PREFERRED_ENCODING ); + } // end try + catch (java.io.UnsupportedEncodingException use) + { + __bytes = _NATIVE_ALPHABET; // Fall back to native encoding + } // end catch + ALPHABET = __bytes; + } // end static + + + /** + * Translates a Base64 value to either its 6-bit reconstruction value + * or a negative number indicating some other meaning. + **/ + private final static byte[] DECODABET = + { + -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 + -5,-5, // Whitespace: Tab and Linefeed + -9,-9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 + -9,-9,-9,-9,-9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9,-9,-9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine + -9,-9,-9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9,-9,-9, // Decimal 62 - 64 + 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' + 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' + -9,-9,-9,-9,-9,-9, // Decimal 91 - 96 + 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' + 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' + -9,-9,-9,-9 // Decimal 123 - 126 + /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + // I think I end up not using the BAD_ENCODING indicator. + //private final static byte BAD_ENCODING = -9; // Indicates error in encoding + private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding + private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding + + private static void closeStream(Closeable stream) { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** Defeats instantiation. */ + private Base64() { + //suppress empty block warning + } + +/* ******** E N C O D I N G M E T H O D S ******** */ + + + /** + * Encodes up to the first three bytes of array <var>threeBytes</var> + * and returns a four-byte array in Base64 notation. + * The actual number of significant bytes in your array is + * given by <var>numSigBytes</var>. + * The array <var>threeBytes</var> needs only be as big as + * <var>numSigBytes</var>. + * Code can reuse a byte array by passing a four-byte array as <var>b4</var>. + * + * @param b4 A reusable byte array to reduce array instantiation + * @param threeBytes the array to convert + * @param numSigBytes the number of significant bytes in your array + * @return four byte array in Base64 notation. + * @since 1.5.1 + */ + private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes ) + { + encode3to4( threeBytes, 0, numSigBytes, b4, 0 ); + return b4; + } // end encode3to4 + + + /** + * Encodes up to three bytes of the array <var>source</var> + * and writes the resulting four Base64 bytes to <var>destination</var>. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * <var>srcOffset</var> and <var>destOffset</var>. + * This method does not check to make sure your arrays + * are large enough to accommodate <var>srcOffset</var> + 3 for + * the <var>source</var> array or <var>destOffset</var> + 4 for + * the <var>destination</var> array. + * The actual number of significant bytes in your array is + * given by <var>numSigBytes</var>. + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @return the <var>destination</var> array + * @since 1.3 + */ + private static byte[] encode3to4( + byte[] source, int srcOffset, int numSigBytes, + byte[] destination, int destOffset ) + { + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = ( numSigBytes > 0 ? ((source[ srcOffset ] << 24) >>> 8) : 0 ) + | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 ) + | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 ); + + switch( numSigBytes ) + { + case 3: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; + destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ]; + return destination; + + case 2: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; + destination[ destOffset + 3 ] = EQUALS_SIGN; + return destination; + + case 1: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = EQUALS_SIGN; + destination[ destOffset + 3 ] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + + + /** + * Serializes an object and returns the Base64-encoded + * version of that serialized object. If the object + * cannot be serialized or there is another error, + * the method will return <tt>null</tt>. + * The object is not GZip-compressed before being encoded. + * + * @param serializableObject The object to encode + * @return The Base64-encoded object + * @since 1.4 + */ + public static String encodeObject( java.io.Serializable serializableObject ) + { + return encodeObject( serializableObject, NO_OPTIONS ); + } // end encodeObject + + + + /** + * Serializes an object and returns the Base64-encoded + * version of that serialized object. If the object + * cannot be serialized or there is another error, + * the method will return <tt>null</tt>. + * <p> + * Valid options:<pre> + * GZIP: gzip-compresses object before encoding it. + * DONT_BREAK_LINES: don't break lines at 76 characters + * <i>Note: Technically, this makes your encoding non-compliant.</i> + * </pre> + * <p> + * Example: <code>encodeObject( myObj, Base64.GZIP )</code> or + * <p> + * Example: <code>encodeObject( myObj, Base64.GZIP | Base64.DONT_BREAK_LINES )</code> + * + * @param serializableObject The object to encode + * @param options Specified options + * @return The Base64-encoded object + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeObject( java.io.Serializable serializableObject, int options ) + { + // Streams + java.io.ByteArrayOutputStream baos = null; + java.io.OutputStream b64os = null; + java.io.ObjectOutputStream oos = null; + java.util.zip.GZIPOutputStream gzos = null; + + // Isolate options + int gzip = (options & GZIP); + int dontBreakLines = (options & DONT_BREAK_LINES); + + try + { + // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream( baos, ENCODE | dontBreakLines ); + + // GZip? + if( gzip == GZIP ) + { + gzos = new java.util.zip.GZIPOutputStream( b64os ); + oos = new java.io.ObjectOutputStream( gzos ); + } // end if: gzip + else + oos = new java.io.ObjectOutputStream( b64os ); + + oos.writeObject( serializableObject ); + } // end try + catch( java.io.IOException e ) + { + e.printStackTrace(); + return null; + } // end catch + finally + { + closeStream(oos); + closeStream(gzos); + closeStream(b64os); + closeStream(baos); + } // end finally + + // Return value according to relevant encoding. + try + { + return new String( baos.toByteArray(), PREFERRED_ENCODING ); + } // end try + catch (java.io.UnsupportedEncodingException uue) + { + return new String( baos.toByteArray() ); + } // end catch + + } // end encode + + + + /** + * Encodes a byte array into Base64 notation. + * Does not GZip-compress data. + * + * @param source The data to convert + * @return encoded base64 representation of source. + * @since 1.4 + */ + public static String encodeBytes( byte[] source ) + { + return encodeBytes( source, 0, source.length, NO_OPTIONS ); + } // end encodeBytes + + + + /** + * Encodes a byte array into Base64 notation. + * <p> + * Valid options:<pre> + * GZIP: gzip-compresses object before encoding it. + * DONT_BREAK_LINES: don't break lines at 76 characters + * <i>Note: Technically, this makes your encoding non-compliant.</i> + * </pre> + * <p> + * Example: <code>encodeBytes( myData, Base64.GZIP )</code> or + * <p> + * Example: <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code> + * + * + * @param source The data to convert + * @param options Specified options + * @return encoded base64 representation of source. + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes( byte[] source, int options ) + { + return encodeBytes( source, 0, source.length, options ); + } // end encodeBytes + + + /** + * Encodes a byte array into Base64 notation. + * Does not GZip-compress data. + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @return encoded base64 representation of source. + * @since 1.4 + */ + public static String encodeBytes( byte[] source, int off, int len ) + { + return encodeBytes( source, off, len, NO_OPTIONS ); + } // end encodeBytes + + + + /** + * Encodes a byte array into Base64 notation. + * <p> + * Valid options:<pre> + * GZIP: gzip-compresses object before encoding it. + * DONT_BREAK_LINES: don't break lines at 76 characters + * <i>Note: Technically, this makes your encoding non-compliant.</i> + * </pre> + * <p> + * Example: <code>encodeBytes( myData, Base64.GZIP )</code> or + * <p> + * Example: <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code> + * + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @return encoded base64 representation of source. + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes( byte[] source, int off, int len, int options ) + { + // Isolate options + int dontBreakLines = ( options & DONT_BREAK_LINES ); + int gzip = ( options & GZIP ); + + // Compress? + if( gzip == GZIP ) + { + java.io.ByteArrayOutputStream baos = null; + java.util.zip.GZIPOutputStream gzos = null; + Base64.OutputStream b64os = null; + + + try + { + // GZip -> Base64 -> ByteArray + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream( baos, ENCODE | dontBreakLines ); + gzos = new java.util.zip.GZIPOutputStream( b64os ); + + gzos.write( source, off, len ); + gzos.close(); + } // end try + catch( java.io.IOException e ) + { + e.printStackTrace(); + return null; + } // end catch + finally + { + closeStream(gzos); + closeStream(b64os); + closeStream(baos); + } // end finally + + // Return value according to relevant encoding. + try + { + return new String( baos.toByteArray(), PREFERRED_ENCODING ); + } // end try + catch (java.io.UnsupportedEncodingException uue) + { + return new String( baos.toByteArray() ); + } // end catch + } // end if: compress + + // Else, don't compress. Better not to use streams at all then. + else + { + // Convert option to boolean in way that code likes it. + boolean breakLines = dontBreakLines == 0; + + int len43 = len * 4 / 3; + byte[] outBuff = new byte[ ( len43 ) // Main 4:3 + + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding + + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for( ; d < len2; d+=3, e+=4 ) + { + encode3to4( source, d+off, 3, outBuff, e ); + + lineLength += 4; + if( breakLines && lineLength == MAX_LINE_LENGTH ) + { + outBuff[e+4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // end for: each piece of array + + if( d < len ) + { + encode3to4( source, d+off, len - d, outBuff, e ); + e += 4; + } // end if: some padding needed + + + // Return value according to relevant encoding. + try + { + return new String( outBuff, 0, e, PREFERRED_ENCODING ); + } // end try + catch (java.io.UnsupportedEncodingException uue) + { + return new String( outBuff, 0, e ); + } // end catch + + } // end else: don't compress + + } // end encodeBytes + + + + + +/* ******** D E C O D I N G M E T H O D S ******** */ + + + /** + * Decodes four bytes from array <var>source</var> + * and writes the resulting bytes (up to three of them) + * to <var>destination</var>. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * <var>srcOffset</var> and <var>destOffset</var>. + * This method does not check to make sure your arrays + * are large enough to accommodate <var>srcOffset</var> + 4 for + * the <var>source</var> array or <var>destOffset</var> + 3 for + * the <var>destination</var> array. + * This method returns the actual number of bytes that + * were converted from the Base64 encoding. + * + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @return the number of decoded bytes converted + * @since 1.3 + */ + private static int decode4to3( byte[] source, int srcOffset, byte[] destination, int destOffset ) + { + // Example: Dk== + if( source[ srcOffset + 2] == EQUALS_SIGN ) + { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); + int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) + | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 ); + + destination[ destOffset ] = (byte)( outBuff >>> 16 ); + return 1; + } + + // Example: DkL= + else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) + { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); + int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) + | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) + | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6 ); + + destination[ destOffset ] = (byte)( outBuff >>> 16 ); + destination[ destOffset + 1 ] = (byte)( outBuff >>> 8 ); + return 2; + } + + // Example: DkLE + else + { + try{ + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) + // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); + int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) + | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) + | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6) + | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF ) ); + + + destination[ destOffset ] = (byte)( outBuff >> 16 ); + destination[ destOffset + 1 ] = (byte)( outBuff >> 8 ); + destination[ destOffset + 2 ] = (byte)( outBuff ); + + return 3; + }catch( Exception e){ + System.out.println(""+source[srcOffset]+ ": " + ( DECODABET[ source[ srcOffset ] ] ) ); + System.out.println(""+source[srcOffset+1]+ ": " + ( DECODABET[ source[ srcOffset + 1 ] ] ) ); + System.out.println(""+source[srcOffset+2]+ ": " + ( DECODABET[ source[ srcOffset + 2 ] ] ) ); + System.out.println(""+source[srcOffset+3]+ ": " + ( DECODABET[ source[ srcOffset + 3 ] ] ) ); + return -1; + } //e nd catch + } + } // end decodeToBytes + + + + + /** + * Very low-level access to decoding ASCII characters in + * the form of a byte array. Does not support automatically + * gunzipping or any other "fancy" features. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @return decoded data + * @since 1.3 + */ + public static byte[] decode( byte[] source, int off, int len ) + { + int len34 = len * 3 / 4; + byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output + int outBuffPosn = 0; + + byte[] b4 = new byte[4]; + int b4Posn = 0; + int i = 0; + byte sbiCrop = 0; + byte sbiDecode = 0; + for( i = off; i < off+len; i++ ) + { + sbiCrop = (byte)(source[i] & 0x7f); // Only the low seven bits + sbiDecode = DECODABET[ sbiCrop ]; + + if( sbiDecode >= WHITE_SPACE_ENC ) // White space, Equals sign or better + { + if( sbiDecode >= EQUALS_SIGN_ENC ) + { + b4[ b4Posn++ ] = sbiCrop; + if( b4Posn > 3 ) + { + outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn ); + b4Posn = 0; + + // If that was the equals sign, break out of 'for' loop + if( sbiCrop == EQUALS_SIGN ) + break; + } // end if: quartet built + + } // end if: equals sign or better + + } // end if: white space, equals sign or better + else + { + System.err.println( "Bad Base64 input character at " + i + ": " + source[i] + "(decimal)" ); + return null; + } // end else: + } // each input character + + byte[] out = new byte[ outBuffPosn ]; + System.arraycopy( outBuff, 0, out, 0, outBuffPosn ); + return out; + } // end decode + + + + + /** + * Decodes data from Base64 notation, automatically + * detecting gzip-compressed data and decompressing it. + * + * @param s the string to decode + * @return the decoded data + * @since 1.4 + */ + public static byte[] decode( String s ) + { + byte[] bytes; + try + { + bytes = s.getBytes( PREFERRED_ENCODING ); + } // end try + catch( java.io.UnsupportedEncodingException uee ) + { + bytes = s.getBytes(); + } // end catch + //</change> + + // Decode + bytes = decode( bytes, 0, bytes.length ); + + + // Check to see if it's gzip-compressed + // GZIP Magic Two-Byte Number: 0x8b1f (35615) + if( bytes != null && bytes.length >= 4 ) + { + + int head = (bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); + if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head ) + { + java.io.ByteArrayInputStream bais = null; + java.util.zip.GZIPInputStream gzis = null; + java.io.ByteArrayOutputStream baos = null; + byte[] buffer = new byte[2048]; + int length = 0; + + try + { + baos = new java.io.ByteArrayOutputStream(); + bais = new java.io.ByteArrayInputStream( bytes ); + gzis = new java.util.zip.GZIPInputStream( bais ); + + while( ( length = gzis.read( buffer ) ) >= 0 ) + { + baos.write(buffer,0,length); + } // end while: reading input + + // No error? Get new bytes. + bytes = baos.toByteArray(); + + } // end try + catch( java.io.IOException e ) + { + // Just return originally-decoded bytes + } // end catch + finally + { + closeStream(baos); + closeStream(gzis); + closeStream(bais); + } // end finally + + } // end if: gzipped + } // end if: bytes.length >= 2 + + return bytes; + } // end decode + + + + + /** + * Attempts to decode Base64 data and deserialize a Java + * Object within. Returns <tt>null</tt> if there was an error. + * + * @param encodedObject The Base64 data to decode + * @return The decoded and deserialized object + * @since 1.5 + */ + public static Object decodeToObject( String encodedObject ) + { + // Decode and gunzip if necessary + byte[] objBytes = decode( encodedObject ); + + java.io.ByteArrayInputStream bais = null; + java.io.ObjectInputStream ois = null; + Object obj = null; + + try + { + bais = new java.io.ByteArrayInputStream( objBytes ); + ois = new java.io.ObjectInputStream( bais ); + + obj = ois.readObject(); + } // end try + catch( java.io.IOException e ) + { + e.printStackTrace(); + } // end catch + catch( java.lang.ClassNotFoundException e ) + { + e.printStackTrace(); + } // end catch + finally + { + closeStream(bais); + closeStream(ois); + } // end finally + + return obj; + } // end decodeObject + + + + /** + * Convenience method for encoding data to a file. + * + * @param dataToEncode byte array of data to encode in base64 form + * @param filename Filename for saving encoded data + * @return <tt>true</tt> if successful, <tt>false</tt> otherwise + * + * @since 2.1 + */ + public static boolean encodeToFile( byte[] dataToEncode, String filename ) + { + boolean success = false; + Base64.OutputStream bos = null; + try + { + bos = new Base64.OutputStream( + new java.io.FileOutputStream( filename ), Base64.ENCODE ); + bos.write( dataToEncode ); + success = true; + } // end try + catch( java.io.IOException e ) + { + + success = false; + } // end catch: IOException + finally + { + closeStream(bos); + } // end finally + + return success; + } // end encodeToFile + + + /** + * Convenience method for decoding data to a file. + * + * @param dataToDecode Base64-encoded data as a string + * @param filename Filename for saving decoded data + * @return <tt>true</tt> if successful, <tt>false</tt> otherwise + * + * @since 2.1 + */ + public static boolean decodeToFile( String dataToDecode, String filename ) + { + boolean success = false; + Base64.OutputStream bos = null; + try + { + bos = new Base64.OutputStream( + new java.io.FileOutputStream( filename ), Base64.DECODE ); + bos.write( dataToDecode.getBytes( PREFERRED_ENCODING ) ); + success = true; + } // end try + catch( java.io.IOException e ) + { + success = false; + } // end catch: IOException + finally + { + closeStream(bos); + } // end finally + + return success; + } // end decodeToFile + + + + + /** + * Convenience method for reading a base64-encoded + * file and decoding it. + * + * @param filename Filename for reading encoded data + * @return decoded byte array or null if unsuccessful + * + * @since 2.1 + */ + public static byte[] decodeFromFile( String filename ) + { + byte[] decodedData = null; + Base64.InputStream bis = null; + try + { + // Set up some useful variables + java.io.File file = new java.io.File( filename ); + byte[] buffer = null; + int length = 0; + int numBytes = 0; + + // Check for size of file + if( file.length() > Integer.MAX_VALUE ) + { + System.err.println( "File is too big for this convenience method (" + file.length() + " bytes)." ); + return null; + } // end if: file too big for int index + buffer = new byte[ (int)file.length() ]; + + // Open a stream + bis = new Base64.InputStream( + new java.io.BufferedInputStream( + new java.io.FileInputStream( file ) ), Base64.DECODE ); + + // Read until done + while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) + length += numBytes; + + // Save in a variable to return + decodedData = new byte[ length ]; + System.arraycopy( buffer, 0, decodedData, 0, length ); + + } // end try + catch( java.io.IOException e ) + { + System.err.println( "Error decoding from file " + filename ); + } // end catch: IOException + finally + { + closeStream(bis); + } // end finally + + return decodedData; + } // end decodeFromFile + + + + /** + * Convenience method for reading a binary file + * and base64-encoding it. + * + * @param filename Filename for reading binary data + * @return base64-encoded string or null if unsuccessful + * + * @since 2.1 + */ + public static String encodeFromFile( String filename ) + { + String encodedData = null; + Base64.InputStream bis = null; + try + { + // Set up some useful variables + java.io.File file = new java.io.File( filename ); + byte[] buffer = new byte[ (int)(file.length() * 1.4) ]; + int length = 0; + int numBytes = 0; + + // Open a stream + bis = new Base64.InputStream( + new java.io.BufferedInputStream( + new java.io.FileInputStream( file ) ), Base64.ENCODE ); + + // Read until done + while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) + length += numBytes; + + // Save in a variable to return + encodedData = new String( buffer, 0, length, Base64.PREFERRED_ENCODING ); + + } // end try + catch( java.io.IOException e ) + { + System.err.println( "Error encoding from file " + filename ); + } // end catch: IOException + finally + { + closeStream(bis); + } // end finally + + return encodedData; + } // end encodeFromFile + + + + + /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ + + + + /** + * A {@link Base64.InputStream} will read data from another + * <tt>java.io.InputStream</tt>, given in the constructor, + * and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class InputStream extends java.io.FilterInputStream + { + private boolean encode; // Encoding or decoding + private int position; // Current position in the buffer + private byte[] buffer; // Small buffer holding converted data + private int bufferLength; // Length of buffer (3 or 4) + private int numSigBytes; // Number of meaningful bytes in the buffer + private int lineLength; + private boolean breakLines; // Break lines at less than 80 characters + + + /** + * Constructs a {@link Base64.InputStream} in DECODE mode. + * + * @param in the <tt>java.io.InputStream</tt> from which to read data. + * @since 1.3 + */ + public InputStream( java.io.InputStream in ) + { + this( in, DECODE ); + } // end constructor + + + /** + * Constructs a {@link Base64.InputStream} in + * either ENCODE or DECODE mode. + * <p> + * Valid options:<pre> + * ENCODE or DECODE: Encode or Decode as data is read. + * DONT_BREAK_LINES: don't break lines at 76 characters + * (only meaningful when encoding) + * <i>Note: Technically, this makes your encoding non-compliant.</i> + * </pre> + * <p> + * Example: <code>new Base64.InputStream( in, Base64.DECODE )</code> + * + * + * @param in the <tt>java.io.InputStream</tt> from which to read data. + * @param options Specified options + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public InputStream( java.io.InputStream in, int options ) + { + super( in ); + this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; + this.encode = (options & ENCODE) == ENCODE; + this.bufferLength = encode ? 4 : 3; + this.buffer = new byte[ bufferLength ]; + this.position = -1; + this.lineLength = 0; + } // end constructor + + /** + * Reads enough of the input stream to convert + * to/from Base64 and returns the next byte. + * + * @return next byte + * @since 1.3 + */ + public int read() throws java.io.IOException + { + // Do we need to get data? + if( position < 0 ) + { + if( encode ) + { + byte[] b3 = new byte[3]; + int numBinaryBytes = 0; + for( int i = 0; i < 3; i++ ) + { + try + { + int b = in.read(); + + // If end of stream, b is -1. + if( b >= 0 ) + { + b3[i] = (byte)b; + numBinaryBytes++; + } // end if: not end of stream + + } // end try: read + catch( java.io.IOException e ) + { + // Only a problem if we got no data at all. + if( i == 0 ) + throw e; + + } // end catch + } // end for: each needed input byte + + if( numBinaryBytes > 0 ) + { + encode3to4( b3, 0, numBinaryBytes, buffer, 0 ); + position = 0; + numSigBytes = 4; + } // end if: got data + else + { + return -1; + } // end else + } // end if: encoding + + // Else decoding + else + { + byte[] b4 = new byte[4]; + int i = 0; + for( i = 0; i < 4; i++ ) + { + // Read four "meaningful" bytes: + int b = 0; + do{ b = in.read(); } + while( b >= 0 && DECODABET[ b & 0x7f ] <= WHITE_SPACE_ENC ); + + if( b < 0 ) + break; // Reads a -1 if end of stream + + b4[i] = (byte)b; + } // end for: each needed input byte + + if( i == 4 ) + { + numSigBytes = decode4to3( b4, 0, buffer, 0 ); + position = 0; + } // end if: got four characters + else if( i == 0 ){ + return -1; + } // end else if: also padded correctly + else + { + // Must have broken out from above. + throw new java.io.IOException( "Improperly padded Base64 input." ); + } // end + + } // end else: decode + } // end else: get data + + // Got data? + if( position >= 0 ) + { + // End of relevant data? + if( /*!encode &&*/ position >= numSigBytes ) + return -1; + + if( encode && breakLines && lineLength >= MAX_LINE_LENGTH ) + { + lineLength = 0; + return '\n'; + } // end if + else + { + lineLength++; // This isn't important when decoding + // but throwing an extra "if" seems + // just as wasteful. + + int b = buffer[ position++ ]; + + if( position >= bufferLength ) + position = -1; + + return b & 0xFF; // This is how you "cast" a byte that's + // intended to be unsigned. + } // end else + } // end if: position >= 0 + + // Else error + else + { + // When JDK1.4 is more accepted, use an assertion here. + throw new java.io.IOException( "Error in Base64 code reading stream." ); + } // end else + } // end read + + + /** + * Calls {@link #read()} repeatedly until the end of stream + * is reached or <var>len</var> bytes are read. + * Returns number of bytes read into array or -1 if + * end of stream is encountered. + * + * @param dest array to hold values + * @param off offset for array + * @param len max number of bytes to read into array + * @return bytes read into array or -1 if end of stream is encountered. + * @since 1.3 + */ + public int read( byte[] dest, int off, int len ) throws java.io.IOException + { + int i; + int b; + for( i = 0; i < len; i++ ) + { + b = read(); + + //if( b < 0 && i == 0 ) + // return -1; + + if( b >= 0 ) + dest[off + i] = (byte)b; + else if( i == 0 ) + return -1; + else + break; // Out of 'for' loop + } // end for: each byte read + return i; + } // end read + + } // end inner class InputStream + + + + + + + /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ + + + + /** + * A {@link Base64.OutputStream} will write data to another + * <tt>java.io.OutputStream</tt>, given in the constructor, + * and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class OutputStream extends java.io.FilterOutputStream + { + private boolean encode; + private int position; + private byte[] buffer; + private int bufferLength; + private int lineLength; + private boolean breakLines; + private byte[] b4; // Scratch used in a few places + private boolean suspendEncoding; + + /** + * Constructs a {@link Base64.OutputStream} in ENCODE mode. + * + * @param out the <tt>java.io.OutputStream</tt> to which data will be written. + * @since 1.3 + */ + public OutputStream( java.io.OutputStream out ) + { + this( out, ENCODE ); + } // end constructor + + + /** + * Constructs a {@link Base64.OutputStream} in + * either ENCODE or DECODE mode. + * <p> + * Valid options:<pre> + * ENCODE or DECODE: Encode or Decode as data is read. + * DONT_BREAK_LINES: don't break lines at 76 characters + * (only meaningful when encoding) + * <i>Note: Technically, this makes your encoding non-compliant.</i> + * </pre> + * <p> + * Example: <code>new Base64.OutputStream( out, Base64.ENCODE )</code> + * + * @param out the <tt>java.io.OutputStream</tt> to which data will be written. + * @param options Specified options. + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DONT_BREAK_LINES + * @since 1.3 + */ + public OutputStream( java.io.OutputStream out, int options ) + { + super( out ); + this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; + this.encode = (options & ENCODE) == ENCODE; + this.bufferLength = encode ? 3 : 4; + this.buffer = new byte[ bufferLength ]; + this.position = 0; + this.lineLength = 0; + this.suspendEncoding = false; + this.b4 = new byte[4]; + } // end constructor + + + /** + * Writes the byte to the output stream after + * converting to/from Base64 notation. + * When encoding, bytes are buffered three + * at a time before the output stream actually + * gets a write() call. + * When decoding, bytes are buffered four + * at a time. + * + * @param theByte the byte to write + * @since 1.3 + */ + public void write(int theByte) throws java.io.IOException + { + // Encoding suspended? + if( suspendEncoding ) + { + super.out.write( theByte ); + return; + } // end if: suspended + + // Encode? + if( encode ) + { + buffer[ position++ ] = (byte)theByte; + if( position >= bufferLength ) // Enough to encode. + { + out.write( encode3to4( b4, buffer, bufferLength ) ); + + lineLength += 4; + if( breakLines && lineLength >= MAX_LINE_LENGTH ) + { + out.write( NEW_LINE ); + lineLength = 0; + } // end if: end of line + + position = 0; + } // end if: enough to output + } // end if: encoding + + // Else, Decoding + else + { + // Meaningful Base64 character? + if( DECODABET[ theByte & 0x7f ] > WHITE_SPACE_ENC ) + { + buffer[ position++ ] = (byte)theByte; + if( position >= bufferLength ) // Enough to output. + { + int len = Base64.decode4to3( buffer, 0, b4, 0 ); + out.write( b4, 0, len ); + //out.write( Base64.decode4to3( buffer ) ); + position = 0; + } // end if: enough to output + } // end if: meaningful base64 character + else if( DECODABET[ theByte & 0x7f ] != WHITE_SPACE_ENC ) + { + throw new java.io.IOException( "Invalid character in Base64 data." ); + } // end else: not white space either + } // end else: decoding + } // end write + + + + /** + * Calls {@link #write(int)} repeatedly until <var>len</var> + * bytes are written. + * + * @param theBytes array from which to read bytes + * @param off offset for array + * @param len max number of bytes to read into array + * @since 1.3 + */ + public void write( byte[] theBytes, int off, int len ) throws java.io.IOException + { + // Encoding suspended? + if( suspendEncoding ) + { + super.out.write( theBytes, off, len ); + return; + } // end if: suspended + + for( int i = 0; i < len; i++ ) + { + write( theBytes[ off + i ] ); + } // end for: each byte written + + } // end write + + + + /** + * Method added by PHIL. [Thanks, PHIL. -Rob] + * This pads the buffer without closing the stream. + * @throws java.io.IOException input was not properly padded. + */ + public void flushBase64() throws java.io.IOException + { + if( position > 0 ) + { + if( encode ) + { + out.write( encode3to4( b4, buffer, position ) ); + position = 0; + } // end if: encoding + else + { + throw new java.io.IOException( "Base64 input not properly padded." ); + } // end else: decoding + } // end if: buffer partially full + + } // end flush + + + /** + * Flushes and closes (I think, in the superclass) the stream. + * + * @since 1.3 + */ + public void close() throws java.io.IOException + { + // 1. Ensure that pending characters are written + flushBase64(); + + // 2. Actually close the stream + // Base class both flushes and closes. + super.close(); + + buffer = null; + out = null; + } // end close + + + + /** + * Suspends encoding of the stream. + * May be helpful if you need to embed a piece of + * base640-encoded data in a stream. + * + * @throws java.io.IOException input was not properly padded. + * @since 1.5.1 + */ + public void suspendEncoding() throws java.io.IOException + { + flushBase64(); + this.suspendEncoding = true; + } // end suspendEncoding + + + /** + * Resumes encoding of the stream. + * May be helpful if you need to embed a piece of + * base640-encoded data in a stream. + * + * @since 1.5.1 + */ + public void resumeEncoding() + { + this.suspendEncoding = false; + } // end resumeEncoding + + + + } // end inner class OutputStream + + +} // end class Base64 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java new file mode 100644 index 0000000000..a52a6530ef --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +import java.io.File; +import java.security.AccessController; +import java.security.PrivilegedAction; + +/** Abstraction to support various file system operations not in Java. */ +public abstract class FS { + /** The implementation selected for this operating system and JRE. */ + public static final FS INSTANCE; + + static { + if (FS_Win32.detect()) { + if (FS_Win32_Cygwin.detect()) + INSTANCE = new FS_Win32_Cygwin(); + else + INSTANCE = new FS_Win32(); + } else if (FS_POSIX_Java6.detect()) + INSTANCE = new FS_POSIX_Java6(); + else + INSTANCE = new FS_POSIX_Java5(); + } + + /** + * Does this operating system and JRE support the execute flag on files? + * + * @return true if this implementation can provide reasonably accurate + * executable bit information; false otherwise. + */ + public abstract boolean supportsExecute(); + + /** + * Determine if the file is executable (or not). + * <p> + * Not all platforms and JREs support executable flags on files. If the + * feature is unsupported this method will always return false. + * + * @param f + * abstract path to test. + * @return true if the file is believed to be executable by the user. + */ + public abstract boolean canExecute(File f); + + /** + * Set a file to be executable by the user. + * <p> + * Not all platforms and JREs support executable flags on files. If the + * feature is unsupported this method will always return false and no + * changes will be made to the file specified. + * + * @param f + * path to modify the executable status of. + * @param canExec + * true to enable execution; false to disable it. + * @return true if the change succeeded; false otherwise. + */ + public abstract boolean setExecute(File f, boolean canExec); + + /** + * Resolve this file to its actual path name that the JRE can use. + * <p> + * This method can be relatively expensive. Computing a translation may + * require forking an external process per path name translated. Callers + * should try to minimize the number of translations necessary by caching + * the results. + * <p> + * Not all platforms and JREs require path name translation. Currently only + * Cygwin on Win32 require translation for Cygwin based paths. + * + * @param dir + * directory relative to which the path name is. + * @param name + * path name to translate. + * @return the translated path. <code>new File(dir,name)</code> if this + * platform does not require path name translation. + */ + public static File resolve(final File dir, final String name) { + return INSTANCE.resolveImpl(dir, name); + } + + /** + * Resolve this file to its actual path name that the JRE can use. + * <p> + * This method can be relatively expensive. Computing a translation may + * require forking an external process per path name translated. Callers + * should try to minimize the number of translations necessary by caching + * the results. + * <p> + * Not all platforms and JREs require path name translation. Currently only + * Cygwin on Win32 require translation for Cygwin based paths. + * + * @param dir + * directory relative to which the path name is. + * @param name + * path name to translate. + * @return the translated path. <code>new File(dir,name)</code> if this + * platform does not require path name translation. + */ + protected File resolveImpl(final File dir, final String name) { + final File abspn = new File(name); + if (abspn.isAbsolute()) + return abspn; + return new File(dir, name); + } + + /** + * Determine the user's home directory (location where preferences are). + * <p> + * This method can be expensive on the first invocation if path name + * translation is required. Subsequent invocations return a cached result. + * <p> + * Not all platforms and JREs require path name translation. Currently only + * Cygwin on Win32 requires translation of the Cygwin HOME directory. + * + * @return the user's home directory; null if the user does not have one. + */ + public static File userHome() { + return USER_HOME.home; + } + + private static class USER_HOME { + static final File home = INSTANCE.userHomeImpl(); + } + + /** + * Determine the user's home directory (location where preferences are). + * + * @return the user's home directory; null if the user does not have one. + */ + protected File userHomeImpl() { + final String home = AccessController + .doPrivileged(new PrivilegedAction<String>() { + public String run() { + return System.getProperty("user.home"); + } + }); + if (home == null || home.length() == 0) + return null; + return new File(home).getAbsoluteFile(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX_Java5.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX_Java5.java new file mode 100644 index 0000000000..4ce0366fc8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX_Java5.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +import java.io.File; + +class FS_POSIX_Java5 extends FS { + public boolean supportsExecute() { + return false; + } + + public boolean canExecute(final File f) { + return false; + } + + public boolean setExecute(final File f, final boolean canExec) { + return false; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX_Java6.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX_Java6.java new file mode 100644 index 0000000000..8a86d2e65f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX_Java6.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2007, Robin Rosenberg <me@lathund.dewire.com> + * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +class FS_POSIX_Java6 extends FS { + private static final Method canExecute; + + private static final Method setExecute; + + static { + canExecute = needMethod(File.class, "canExecute"); + setExecute = needMethod(File.class, "setExecutable", Boolean.TYPE); + } + + static boolean detect() { + return canExecute != null && setExecute != null; + } + + private static Method needMethod(final Class<?> on, final String name, + final Class<?>... args) { + try { + return on.getMethod(name, args); + } catch (SecurityException e) { + return null; + } catch (NoSuchMethodException e) { + return null; + } + } + + public boolean supportsExecute() { + return true; + } + + public boolean canExecute(final File f) { + try { + final Object r = canExecute.invoke(f, (Object[]) null); + return ((Boolean) r).booleanValue(); + } catch (IllegalArgumentException e) { + throw new Error(e); + } catch (IllegalAccessException e) { + throw new Error(e); + } catch (InvocationTargetException e) { + throw new Error(e); + } + } + + public boolean setExecute(final File f, final boolean canExec) { + try { + final Object r; + r = setExecute.invoke(f, new Object[] { Boolean.valueOf(canExec) }); + return ((Boolean) r).booleanValue(); + } catch (IllegalArgumentException e) { + throw new Error(e); + } catch (IllegalAccessException e) { + throw new Error(e); + } catch (InvocationTargetException e) { + throw new Error(e); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java new file mode 100644 index 0000000000..79bf1e82e8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +import java.io.File; +import java.security.AccessController; +import java.security.PrivilegedAction; + +class FS_Win32 extends FS { + static boolean detect() { + final String osDotName = AccessController + .doPrivileged(new PrivilegedAction<String>() { + public String run() { + return System.getProperty("os.name"); + } + }); + return osDotName != null + && StringUtils.toLowerCase(osDotName).indexOf("windows") != -1; + } + + public boolean supportsExecute() { + return false; + } + + public boolean canExecute(final File f) { + return false; + } + + public boolean setExecute(final File f, final boolean canExec) { + return false; + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java new file mode 100644 index 0000000000..f727084860 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.security.AccessController; +import java.security.PrivilegedAction; + +class FS_Win32_Cygwin extends FS_Win32 { + private static String cygpath; + + static boolean detect() { + final String path = AccessController + .doPrivileged(new PrivilegedAction<String>() { + public String run() { + return System.getProperty("java.library.path"); + } + }); + if (path == null) + return false; + for (final String p : path.split(";")) { + final File e = new File(p, "cygpath.exe"); + if (e.isFile()) { + cygpath = e.getAbsolutePath(); + return true; + } + } + return false; + } + + protected File resolveImpl(final File dir, final String pn) { + try { + final Process p; + + p = Runtime.getRuntime().exec( + new String[] { cygpath, "--windows", "--absolute", pn }, + null, dir); + p.getOutputStream().close(); + + final BufferedReader lineRead = new BufferedReader( + new InputStreamReader(p.getInputStream(), "UTF-8")); + String r = null; + try { + r = lineRead.readLine(); + } finally { + lineRead.close(); + } + + for (;;) { + try { + if (p.waitFor() == 0 && r != null && r.length() > 0) + return new File(r); + break; + } catch (InterruptedException ie) { + // Stop bothering me, I have a zombie to reap. + } + } + } catch (IOException ioe) { + // Fall through and use the default return. + // + } + return super.resolveImpl(dir, pn); + } + + @Override + protected File userHomeImpl() { + final String home = AccessController + .doPrivileged(new PrivilegedAction<String>() { + public String run() { + return System.getenv("HOME"); + } + }); + if (home == null || home.length() == 0) + return super.userHomeImpl(); + return resolveImpl(new File("."), home); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java new file mode 100644 index 0000000000..40134d0e4f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.ConnectException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLEncoder; + +import org.eclipse.jgit.awtui.AwtAuthenticator; + +/** Extra utilities to support usage of HTTP. */ +public class HttpSupport { + /** + * Configure the JRE's standard HTTP based on <code>http_proxy</code>. + * <p> + * The popular libcurl library honors the <code>http_proxy</code> + * environment variable as a means of specifying an HTTP proxy for requests + * made behind a firewall. This is not natively recognized by the JRE, so + * this method can be used by command line utilities to configure the JRE + * before the first request is sent. + * + * @throws MalformedURLException + * the value in <code>http_proxy</code> is unsupportable. + */ + public static void configureHttpProxy() throws MalformedURLException { + final String s = System.getenv("http_proxy"); + if (s == null || s.equals("")) + return; + + final URL u = new URL((s.indexOf("://") == -1) ? "http://" + s : s); + if (!"http".equals(u.getProtocol())) + throw new MalformedURLException("Invalid http_proxy: " + s + + ": Only http supported."); + + final String proxyHost = u.getHost(); + final int proxyPort = u.getPort(); + + System.setProperty("http.proxyHost", proxyHost); + if (proxyPort > 0) + System.setProperty("http.proxyPort", String.valueOf(proxyPort)); + + final String userpass = u.getUserInfo(); + if (userpass != null && userpass.contains(":")) { + final int c = userpass.indexOf(':'); + final String user = userpass.substring(0, c); + final String pass = userpass.substring(c + 1); + AwtAuthenticator.add(new AwtAuthenticator.CachedAuthentication( + proxyHost, proxyPort, user, pass)); + } + } + + /** + * URL encode a value string into an output buffer. + * + * @param urlstr + * the output buffer. + * @param key + * value which must be encoded to protected special characters. + */ + public static void encode(final StringBuilder urlstr, final String key) { + if (key == null || key.length() == 0) + return; + try { + urlstr.append(URLEncoder.encode(key, "UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Could not URL encode to UTF-8", e); + } + } + + /** + * Get the HTTP response code from the request. + * <p> + * Roughly the same as <code>c.getResponseCode()</code> but the + * ConnectException is translated to be more understandable. + * + * @param c + * connection the code should be obtained from. + * @return r HTTP status code, usually 200 to indicate success. See + * {@link HttpURLConnection} for other defined constants. + * @throws IOException + * communications error prevented obtaining the response code. + */ + public static int response(final HttpURLConnection c) throws IOException { + try { + return c.getResponseCode(); + } catch (ConnectException ce) { + final String host = c.getURL().getHost(); + // The standard J2SE error message is not very useful. + // + if ("Connection timed out: connect".equals(ce.getMessage())) + throw new ConnectException("Connection time out: " + host); + throw new ConnectException(ce.getMessage() + " " + host); + } + } + + /** + * Determine the proxy server (if any) needed to obtain a URL. + * + * @param proxySelector + * proxy support for the caller. + * @param u + * location of the server caller wants to talk to. + * @return proxy to communicate with the supplied URL. + * @throws ConnectException + * the proxy could not be computed as the supplied URL could not + * be read. This failure should never occur. + */ + public static Proxy proxyFor(final ProxySelector proxySelector, final URL u) + throws ConnectException { + try { + return proxySelector.select(u.toURI()).get(0); + } catch (URISyntaxException e) { + final ConnectException err; + err = new ConnectException("Cannot determine proxy for " + u); + err.initCause(e); + throw err; + } + } + + private HttpSupport() { + // Utility class only. + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/IntList.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/IntList.java new file mode 100644 index 0000000000..510f2a4db9 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/IntList.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +/** A more efficient List<Integer> using a primitive integer array. */ +public class IntList { + private int[] entries; + + private int count; + + /** Create an empty list with a default capacity. */ + public IntList() { + this(10); + } + + /** + * Create an empty list with the specified capacity. + * + * @param capacity + * number of entries the list can initially hold. + */ + public IntList(final int capacity) { + entries = new int[capacity]; + } + + /** @return number of entries in this list */ + public int size() { + return count; + } + + /** + * @param i + * index to read, must be in the range [0, {@link #size()}). + * @return the number at the specified index + * @throws ArrayIndexOutOfBoundsException + * the index outside the valid range + */ + public int get(final int i) { + if (count <= i) + throw new ArrayIndexOutOfBoundsException(i); + return entries[i]; + } + + /** Empty this list */ + public void clear() { + count = 0; + } + + /** + * Add an entry to the end of the list. + * + * @param n + * the number to add. + */ + public void add(final int n) { + if (count == entries.length) + grow(); + entries[count++] = n; + } + + /** + * Pad the list with entries. + * + * @param toIndex + * index position to stop filling at. 0 inserts no filler. 1 + * ensures the list has a size of 1, adding <code>val</code> if + * the list is currently empty. + * @param val + * value to insert into padded positions. + */ + public void fillTo(int toIndex, final int val) { + while (count < toIndex) + add(val); + } + + private void grow() { + final int[] n = new int[(entries.length + 16) * 3 / 2]; + System.arraycopy(entries, 0, n, 0, count); + entries = n; + } + + public String toString() { + final StringBuilder r = new StringBuilder(); + r.append('['); + for (int i = 0; i < count; i++) { + if (i > 0) + r.append(", "); + r.append(entries[i]); + } + r.append(']'); + return r.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/MutableInteger.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/MutableInteger.java new file mode 100644 index 0000000000..cbe321086c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/MutableInteger.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +/** A boxed integer that can be modified. */ +public final class MutableInteger { + /** Current value of this boxed value. */ + public int value; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/NB.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/NB.java new file mode 100644 index 0000000000..a42871dbc3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/NB.java @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** Conversion utilities for network byte order handling. */ +public final class NB { + /** + * Read an entire local file into memory as a byte array. + * + * @param path + * location of the file to read. + * @return complete contents of the requested local file. + * @throws FileNotFoundException + * the file does not exist. + * @throws IOException + * the file exists, but its contents cannot be read. + */ + public static final byte[] readFully(final File path) + throws FileNotFoundException, IOException { + return readFully(path, Integer.MAX_VALUE); + } + + /** + * Read an entire local file into memory as a byte array. + * + * @param path + * location of the file to read. + * @param max + * maximum number of bytes to read, if the file is larger than + * this limit an IOException is thrown. + * @return complete contents of the requested local file. + * @throws FileNotFoundException + * the file does not exist. + * @throws IOException + * the file exists, but its contents cannot be read. + */ + public static final byte[] readFully(final File path, final int max) + throws FileNotFoundException, IOException { + final FileInputStream in = new FileInputStream(path); + try { + final long sz = in.getChannel().size(); + if (sz > max) + throw new IOException("File is too large: " + path); + final byte[] buf = new byte[(int) sz]; + readFully(in, buf, 0, buf.length); + return buf; + } finally { + try { + in.close(); + } catch (IOException ignored) { + // ignore any close errors, this was a read only stream + } + } + } + + /** + * Read the entire byte array into memory, or throw an exception. + * + * @param fd + * input stream to read the data from. + * @param dst + * buffer that must be fully populated, [off, off+len). + * @param off + * position within the buffer to start writing to. + * @param len + * number of bytes that must be read. + * @throws EOFException + * the stream ended before dst was fully populated. + * @throws IOException + * there was an error reading from the stream. + */ + public static void readFully(final InputStream fd, final byte[] dst, + int off, int len) throws IOException { + while (len > 0) { + final int r = fd.read(dst, off, len); + if (r <= 0) + throw new EOFException("Short read of block."); + off += r; + len -= r; + } + } + + /** + * Read the entire byte array into memory, or throw an exception. + * + * @param fd + * file to read the data from. + * @param pos + * position to read from the file at. + * @param dst + * buffer that must be fully populated, [off, off+len). + * @param off + * position within the buffer to start writing to. + * @param len + * number of bytes that must be read. + * @throws EOFException + * the stream ended before dst was fully populated. + * @throws IOException + * there was an error reading from the stream. + */ + public static void readFully(final FileChannel fd, long pos, + final byte[] dst, int off, int len) throws IOException { + while (len > 0) { + final int r = fd.read(ByteBuffer.wrap(dst, off, len), pos); + if (r <= 0) + throw new EOFException("Short read of block."); + pos += r; + off += r; + len -= r; + } + } + + /** + * Skip an entire region of an input stream. + * <p> + * The input stream's position is moved forward by the number of requested + * bytes, discarding them from the input. This method does not return until + * the exact number of bytes requested has been skipped. + * + * @param fd + * the stream to skip bytes from. + * @param toSkip + * total number of bytes to be discarded. Must be >= 0. + * @throws EOFException + * the stream ended before the requested number of bytes were + * skipped. + * @throws IOException + * there was an error reading from the stream. + */ + public static void skipFully(final InputStream fd, long toSkip) + throws IOException { + while (toSkip > 0) { + final long r = fd.skip(toSkip); + if (r <= 0) + throw new EOFException("Short skip of block"); + toSkip -= r; + } + } + + /** + * Compare a 32 bit unsigned integer stored in a 32 bit signed integer. + * <p> + * This function performs an unsigned compare operation, even though Java + * does not natively support unsigned integer values. Negative numbers are + * treated as larger than positive ones. + * + * @param a + * the first value to compare. + * @param b + * the second value to compare. + * @return < 0 if a < b; 0 if a == b; > 0 if a > b. + */ + public static int compareUInt32(final int a, final int b) { + final int cmp = (a >>> 1) - (b >>> 1); + if (cmp != 0) + return cmp; + return (a & 1) - (b & 1); + } + + /** + * Convert sequence of 2 bytes (network byte order) into unsigned value. + * + * @param intbuf + * buffer to acquire the 2 bytes of data from. + * @param offset + * position within the buffer to begin reading from. This + * position and the next byte after it (for a total of 2 bytes) + * will be read. + * @return unsigned integer value that matches the 16 bits read. + */ + public static int decodeUInt16(final byte[] intbuf, final int offset) { + int r = (intbuf[offset] & 0xff) << 8; + return r | (intbuf[offset + 1] & 0xff); + } + + /** + * Convert sequence of 4 bytes (network byte order) into signed value. + * + * @param intbuf + * buffer to acquire the 4 bytes of data from. + * @param offset + * position within the buffer to begin reading from. This + * position and the next 3 bytes after it (for a total of 4 + * bytes) will be read. + * @return signed integer value that matches the 32 bits read. + */ + public static int decodeInt32(final byte[] intbuf, final int offset) { + int r = intbuf[offset] << 8; + + r |= intbuf[offset + 1] & 0xff; + r <<= 8; + + r |= intbuf[offset + 2] & 0xff; + return (r << 8) | (intbuf[offset + 3] & 0xff); + } + + /** + * Convert sequence of 4 bytes (network byte order) into unsigned value. + * + * @param intbuf + * buffer to acquire the 4 bytes of data from. + * @param offset + * position within the buffer to begin reading from. This + * position and the next 3 bytes after it (for a total of 4 + * bytes) will be read. + * @return unsigned integer value that matches the 32 bits read. + */ + public static long decodeUInt32(final byte[] intbuf, final int offset) { + int low = (intbuf[offset + 1] & 0xff) << 8; + low |= (intbuf[offset + 2] & 0xff); + low <<= 8; + + low |= (intbuf[offset + 3] & 0xff); + return ((long) (intbuf[offset] & 0xff)) << 24 | low; + } + + /** + * Convert sequence of 8 bytes (network byte order) into unsigned value. + * + * @param intbuf + * buffer to acquire the 8 bytes of data from. + * @param offset + * position within the buffer to begin reading from. This + * position and the next 7 bytes after it (for a total of 8 + * bytes) will be read. + * @return unsigned integer value that matches the 64 bits read. + */ + public static long decodeUInt64(final byte[] intbuf, final int offset) { + return (decodeUInt32(intbuf, offset) << 32) + | decodeUInt32(intbuf, offset + 4); + } + + /** + * Write a 16 bit integer as a sequence of 2 bytes (network byte order). + * + * @param intbuf + * buffer to write the 2 bytes of data into. + * @param offset + * position within the buffer to begin writing to. This position + * and the next byte after it (for a total of 2 bytes) will be + * replaced. + * @param v + * the value to write. + */ + public static void encodeInt16(final byte[] intbuf, final int offset, int v) { + intbuf[offset + 1] = (byte) v; + v >>>= 8; + + intbuf[offset] = (byte) v; + } + + /** + * Write a 32 bit integer as a sequence of 4 bytes (network byte order). + * + * @param intbuf + * buffer to write the 4 bytes of data into. + * @param offset + * position within the buffer to begin writing to. This position + * and the next 3 bytes after it (for a total of 4 bytes) will be + * replaced. + * @param v + * the value to write. + */ + public static void encodeInt32(final byte[] intbuf, final int offset, int v) { + intbuf[offset + 3] = (byte) v; + v >>>= 8; + + intbuf[offset + 2] = (byte) v; + v >>>= 8; + + intbuf[offset + 1] = (byte) v; + v >>>= 8; + + intbuf[offset] = (byte) v; + } + + /** + * Write a 64 bit integer as a sequence of 8 bytes (network byte order). + * + * @param intbuf + * buffer to write the 48bytes of data into. + * @param offset + * position within the buffer to begin writing to. This position + * and the next 7 bytes after it (for a total of 8 bytes) will be + * replaced. + * @param v + * the value to write. + */ + public static void encodeInt64(final byte[] intbuf, final int offset, long v) { + intbuf[offset + 7] = (byte) v; + v >>>= 8; + + intbuf[offset + 6] = (byte) v; + v >>>= 8; + + intbuf[offset + 5] = (byte) v; + v >>>= 8; + + intbuf[offset + 4] = (byte) v; + v >>>= 8; + + intbuf[offset + 3] = (byte) v; + v >>>= 8; + + intbuf[offset + 2] = (byte) v; + v >>>= 8; + + intbuf[offset + 1] = (byte) v; + v >>>= 8; + + intbuf[offset] = (byte) v; + } + + private NB() { + // Don't create instances of a static only utility. + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/QuotedString.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/QuotedString.java new file mode 100644 index 0000000000..7e5bde7582 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/QuotedString.java @@ -0,0 +1,368 @@ +/* + * Copyright (C) 2008, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +import java.util.Arrays; + +import org.eclipse.jgit.lib.Constants; + +/** Utility functions related to quoted string handling. */ +public abstract class QuotedString { + /** Quoting style that obeys the rules Git applies to file names */ + public static final GitPathStyle GIT_PATH = new GitPathStyle(); + + /** + * Quoting style used by the Bourne shell. + * <p> + * Quotes are unconditionally inserted during {@link #quote(String)}. This + * protects shell meta-characters like <code>$</code> or <code>~</code> from + * being recognized as special. + */ + public static final BourneStyle BOURNE = new BourneStyle(); + + /** Bourne style, but permits <code>~user</code> at the start of the string. */ + public static final BourneUserPathStyle BOURNE_USER_PATH = new BourneUserPathStyle(); + + /** + * Quote an input string by the quoting rules. + * <p> + * If the input string does not require any quoting, the same String + * reference is returned to the caller. + * <p> + * Otherwise a quoted string is returned, including the opening and closing + * quotation marks at the start and end of the string. If the style does not + * permit raw Unicode characters then the string will first be encoded in + * UTF-8, with unprintable sequences possibly escaped by the rules. + * + * @param in + * any non-null Unicode string. + * @return a quoted string. See above for details. + */ + public abstract String quote(String in); + + /** + * Clean a previously quoted input, decoding the result via UTF-8. + * <p> + * This method must match quote such that: + * + * <pre> + * a.equals(dequote(quote(a))); + * </pre> + * + * is true for any <code>a</code>. + * + * @param in + * a Unicode string to remove quoting from. + * @return the cleaned string. + * @see #dequote(byte[], int, int) + */ + public String dequote(final String in) { + final byte[] b = Constants.encode(in); + return dequote(b, 0, b.length); + } + + /** + * Decode a previously quoted input, scanning a UTF-8 encoded buffer. + * <p> + * This method must match quote such that: + * + * <pre> + * a.equals(dequote(Constants.encode(quote(a)))); + * </pre> + * + * is true for any <code>a</code>. + * <p> + * This method removes any opening/closing quotation marks added by + * {@link #quote(String)}. + * + * @param in + * the input buffer to parse. + * @param offset + * first position within <code>in</code> to scan. + * @param end + * one position past in <code>in</code> to scan. + * @return the cleaned string. + */ + public abstract String dequote(byte[] in, int offset, int end); + + /** + * Quoting style used by the Bourne shell. + * <p> + * Quotes are unconditionally inserted during {@link #quote(String)}. This + * protects shell meta-characters like <code>$</code> or <code>~</code> from + * being recognized as special. + */ + public static class BourneStyle extends QuotedString { + @Override + public String quote(final String in) { + final StringBuilder r = new StringBuilder(); + r.append('\''); + int start = 0, i = 0; + for (; i < in.length(); i++) { + switch (in.charAt(i)) { + case '\'': + case '!': + r.append(in, start, i); + r.append('\''); + r.append('\\'); + r.append(in.charAt(i)); + r.append('\''); + start = i + 1; + break; + } + } + r.append(in, start, i); + r.append('\''); + return r.toString(); + } + + @Override + public String dequote(final byte[] in, int ip, final int ie) { + boolean inquote = false; + final byte[] r = new byte[ie - ip]; + int rPtr = 0; + while (ip < ie) { + final byte b = in[ip++]; + switch (b) { + case '\'': + inquote = !inquote; + continue; + case '\\': + if (inquote || ip == ie) + r[rPtr++] = b; // literal within a quote + else + r[rPtr++] = in[ip++]; + continue; + default: + r[rPtr++] = b; + continue; + } + } + return RawParseUtils.decode(Constants.CHARSET, r, 0, rPtr); + } + } + + /** Bourne style, but permits <code>~user</code> at the start of the string. */ + public static class BourneUserPathStyle extends BourneStyle { + @Override + public String quote(final String in) { + if (in.matches("^~[A-Za-z0-9_-]+$")) { + // If the string is just "~user" we can assume they + // mean "~user/". + // + return in + "/"; + } + + if (in.matches("^~[A-Za-z0-9_-]*/.*$")) { + // If the string is of "~/path" or "~user/path" + // we must not escape ~/ or ~user/ from the shell. + // + final int i = in.indexOf('/') + 1; + if (i == in.length()) + return in; + return in.substring(0, i) + super.quote(in.substring(i)); + } + + return super.quote(in); + } + } + + /** Quoting style that obeys the rules Git applies to file names */ + public static final class GitPathStyle extends QuotedString { + private static final byte[] quote; + static { + quote = new byte[128]; + Arrays.fill(quote, (byte) -1); + + for (int i = '0'; i <= '9'; i++) + quote[i] = 0; + for (int i = 'a'; i <= 'z'; i++) + quote[i] = 0; + for (int i = 'A'; i <= 'Z'; i++) + quote[i] = 0; + quote[' '] = 0; + quote['+'] = 0; + quote[','] = 0; + quote['-'] = 0; + quote['.'] = 0; + quote['/'] = 0; + quote['='] = 0; + quote['_'] = 0; + quote['^'] = 0; + + quote['\u0007'] = 'a'; + quote['\b'] = 'b'; + quote['\f'] = 'f'; + quote['\n'] = 'n'; + quote['\r'] = 'r'; + quote['\t'] = 't'; + quote['\u000B'] = 'v'; + quote['\\'] = '\\'; + quote['"'] = '"'; + } + + @Override + public String quote(final String instr) { + if (instr.length() == 0) + return "\"\""; + boolean reuse = true; + final byte[] in = Constants.encode(instr); + final StringBuilder r = new StringBuilder(2 + in.length); + r.append('"'); + for (int i = 0; i < in.length; i++) { + final int c = in[i] & 0xff; + if (c < quote.length) { + final byte style = quote[c]; + if (style == 0) { + r.append((char) c); + continue; + } + if (style > 0) { + reuse = false; + r.append('\\'); + r.append((char) style); + continue; + } + } + + reuse = false; + r.append('\\'); + r.append((char) (((c >> 6) & 03) + '0')); + r.append((char) (((c >> 3) & 07) + '0')); + r.append((char) (((c >> 0) & 07) + '0')); + } + if (reuse) + return instr; + r.append('"'); + return r.toString(); + } + + @Override + public String dequote(final byte[] in, final int inPtr, final int inEnd) { + if (2 <= inEnd - inPtr && in[inPtr] == '"' && in[inEnd - 1] == '"') + return dq(in, inPtr + 1, inEnd - 1); + return RawParseUtils.decode(Constants.CHARSET, in, inPtr, inEnd); + } + + private static String dq(final byte[] in, int inPtr, final int inEnd) { + final byte[] r = new byte[inEnd - inPtr]; + int rPtr = 0; + while (inPtr < inEnd) { + final byte b = in[inPtr++]; + if (b != '\\') { + r[rPtr++] = b; + continue; + } + + if (inPtr == inEnd) { + // Lone trailing backslash. Treat it as a literal. + // + r[rPtr++] = '\\'; + break; + } + + switch (in[inPtr++]) { + case 'a': + r[rPtr++] = 0x07 /* \a = BEL */; + continue; + case 'b': + r[rPtr++] = '\b'; + continue; + case 'f': + r[rPtr++] = '\f'; + continue; + case 'n': + r[rPtr++] = '\n'; + continue; + case 'r': + r[rPtr++] = '\r'; + continue; + case 't': + r[rPtr++] = '\t'; + continue; + case 'v': + r[rPtr++] = 0x0B/* \v = VT */; + continue; + + case '\\': + case '"': + r[rPtr++] = in[inPtr - 1]; + continue; + + case '0': + case '1': + case '2': + case '3': { + int cp = in[inPtr - 1] - '0'; + while (inPtr < inEnd) { + final byte c = in[inPtr]; + if ('0' <= c && c <= '7') { + cp <<= 3; + cp |= c - '0'; + inPtr++; + } else { + break; + } + } + r[rPtr++] = (byte) cp; + continue; + } + + default: + // Any other code is taken literally. + // + r[rPtr++] = '\\'; + r[rPtr++] = in[inPtr - 1]; + continue; + } + } + + return RawParseUtils.decode(Constants.CHARSET, r, 0, rPtr); + } + + private GitPathStyle() { + // Singleton + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawCharSequence.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawCharSequence.java new file mode 100644 index 0000000000..c89705cb6d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawCharSequence.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +/** + * A rough character sequence around a raw byte buffer. + * <p> + * Characters are assumed to be 8-bit US-ASCII. + */ +public final class RawCharSequence implements CharSequence { + /** A zero-length character sequence. */ + public static final RawCharSequence EMPTY = new RawCharSequence(null, 0, 0); + + final byte[] buffer; + + final int startPtr; + + final int endPtr; + + /** + * Create a rough character sequence around the raw byte buffer. + * + * @param buf + * buffer to scan. + * @param start + * starting position for the sequence. + * @param end + * ending position for the sequence. + */ + public RawCharSequence(final byte[] buf, final int start, final int end) { + buffer = buf; + startPtr = start; + endPtr = end; + } + + public char charAt(final int index) { + return (char) (buffer[startPtr + index] & 0xff); + } + + public int length() { + return endPtr - startPtr; + } + + public CharSequence subSequence(final int start, final int end) { + return new RawCharSequence(buffer, startPtr + start, startPtr + end); + } + + @Override + public String toString() { + final int n = length(); + final StringBuilder b = new StringBuilder(n); + for (int i = 0; i < n; i++) + b.append(charAt(i)); + return b.toString(); + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java new file mode 100644 index 0000000000..9254eb3d79 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java @@ -0,0 +1,1016 @@ +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +import static org.eclipse.jgit.lib.ObjectChecker.author; +import static org.eclipse.jgit.lib.ObjectChecker.committer; +import static org.eclipse.jgit.lib.ObjectChecker.encoding; +import static org.eclipse.jgit.lib.ObjectChecker.tagger; + +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.util.Arrays; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.PersonIdent; + +/** Handy utility functions to parse raw object contents. */ +public final class RawParseUtils { + private static final byte[] digits10; + + private static final byte[] digits16; + + private static final byte[] footerLineKeyChars; + + static { + digits10 = new byte['9' + 1]; + Arrays.fill(digits10, (byte) -1); + for (char i = '0'; i <= '9'; i++) + digits10[i] = (byte) (i - '0'); + + digits16 = new byte['f' + 1]; + Arrays.fill(digits16, (byte) -1); + for (char i = '0'; i <= '9'; i++) + digits16[i] = (byte) (i - '0'); + for (char i = 'a'; i <= 'f'; i++) + digits16[i] = (byte) ((i - 'a') + 10); + for (char i = 'A'; i <= 'F'; i++) + digits16[i] = (byte) ((i - 'A') + 10); + + footerLineKeyChars = new byte['z' + 1]; + footerLineKeyChars['-'] = 1; + for (char i = '0'; i <= '9'; i++) + footerLineKeyChars[i] = 1; + for (char i = 'A'; i <= 'Z'; i++) + footerLineKeyChars[i] = 1; + for (char i = 'a'; i <= 'z'; i++) + footerLineKeyChars[i] = 1; + } + + /** + * Determine if b[ptr] matches src. + * + * @param b + * the buffer to scan. + * @param ptr + * first position within b, this should match src[0]. + * @param src + * the buffer to test for equality with b. + * @return ptr + src.length if b[ptr..src.length] == src; else -1. + */ + public static final int match(final byte[] b, int ptr, final byte[] src) { + if (ptr + src.length > b.length) + return -1; + for (int i = 0; i < src.length; i++, ptr++) + if (b[ptr] != src[i]) + return -1; + return ptr; + } + + private static final byte[] base10byte = { '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9' }; + + /** + * Format a base 10 numeric into a temporary buffer. + * <p> + * Formatting is performed backwards. The method starts at offset + * <code>o-1</code> and ends at <code>o-1-digits</code>, where + * <code>digits</code> is the number of positions necessary to store the + * base 10 value. + * <p> + * The argument and return values from this method make it easy to chain + * writing, for example: + * </p> + * + * <pre> + * final byte[] tmp = new byte[64]; + * int ptr = tmp.length; + * tmp[--ptr] = '\n'; + * ptr = RawParseUtils.formatBase10(tmp, ptr, 32); + * tmp[--ptr] = ' '; + * ptr = RawParseUtils.formatBase10(tmp, ptr, 18); + * tmp[--ptr] = 0; + * final String str = new String(tmp, ptr, tmp.length - ptr); + * </pre> + * + * @param b + * buffer to write into. + * @param o + * one offset past the location where writing will begin; writing + * proceeds towards lower index values. + * @param value + * the value to store. + * @return the new offset value <code>o</code>. This is the position of + * the last byte written. Additional writing should start at one + * position earlier. + */ + public static int formatBase10(final byte[] b, int o, int value) { + if (value == 0) { + b[--o] = '0'; + return o; + } + final boolean isneg = value < 0; + while (value != 0) { + b[--o] = base10byte[value % 10]; + value /= 10; + } + if (isneg) + b[--o] = '-'; + return o; + } + + /** + * Parse a base 10 numeric from a sequence of ASCII digits into an int. + * <p> + * Digit sequences can begin with an optional run of spaces before the + * sequence, and may start with a '+' or a '-' to indicate sign position. + * Any other characters will cause the method to stop and return the current + * result to the caller. + * + * @param b + * buffer to scan. + * @param ptr + * position within buffer to start parsing digits at. + * @param ptrResult + * optional location to return the new ptr value through. If null + * the ptr value will be discarded. + * @return the value at this location; 0 if the location is not a valid + * numeric. + */ + public static final int parseBase10(final byte[] b, int ptr, + final MutableInteger ptrResult) { + int r = 0; + int sign = 0; + try { + final int sz = b.length; + while (ptr < sz && b[ptr] == ' ') + ptr++; + if (ptr >= sz) + return 0; + + switch (b[ptr]) { + case '-': + sign = -1; + ptr++; + break; + case '+': + ptr++; + break; + } + + while (ptr < sz) { + final byte v = digits10[b[ptr]]; + if (v < 0) + break; + r = (r * 10) + v; + ptr++; + } + } catch (ArrayIndexOutOfBoundsException e) { + // Not a valid digit. + } + if (ptrResult != null) + ptrResult.value = ptr; + return sign < 0 ? -r : r; + } + + /** + * Parse a base 10 numeric from a sequence of ASCII digits into a long. + * <p> + * Digit sequences can begin with an optional run of spaces before the + * sequence, and may start with a '+' or a '-' to indicate sign position. + * Any other characters will cause the method to stop and return the current + * result to the caller. + * + * @param b + * buffer to scan. + * @param ptr + * position within buffer to start parsing digits at. + * @param ptrResult + * optional location to return the new ptr value through. If null + * the ptr value will be discarded. + * @return the value at this location; 0 if the location is not a valid + * numeric. + */ + public static final long parseLongBase10(final byte[] b, int ptr, + final MutableInteger ptrResult) { + long r = 0; + int sign = 0; + try { + final int sz = b.length; + while (ptr < sz && b[ptr] == ' ') + ptr++; + if (ptr >= sz) + return 0; + + switch (b[ptr]) { + case '-': + sign = -1; + ptr++; + break; + case '+': + ptr++; + break; + } + + while (ptr < sz) { + final byte v = digits10[b[ptr]]; + if (v < 0) + break; + r = (r * 10) + v; + ptr++; + } + } catch (ArrayIndexOutOfBoundsException e) { + // Not a valid digit. + } + if (ptrResult != null) + ptrResult.value = ptr; + return sign < 0 ? -r : r; + } + + /** + * Parse 4 character base 16 (hex) formatted string to unsigned integer. + * <p> + * The number is read in network byte order, that is, most significant + * nybble first. + * + * @param bs + * buffer to parse digits from; positions {@code [p, p+4)} will + * be parsed. + * @param p + * first position within the buffer to parse. + * @return the integer value. + * @throws ArrayIndexOutOfBoundsException + * if the string is not hex formatted. + */ + public static final int parseHexInt16(final byte[] bs, final int p) { + int r = digits16[bs[p]] << 4; + + r |= digits16[bs[p + 1]]; + r <<= 4; + + r |= digits16[bs[p + 2]]; + r <<= 4; + + r |= digits16[bs[p + 3]]; + if (r < 0) + throw new ArrayIndexOutOfBoundsException(); + return r; + } + + /** + * Parse 8 character base 16 (hex) formatted string to unsigned integer. + * <p> + * The number is read in network byte order, that is, most significant + * nybble first. + * + * @param bs + * buffer to parse digits from; positions {@code [p, p+8)} will + * be parsed. + * @param p + * first position within the buffer to parse. + * @return the integer value. + * @throws ArrayIndexOutOfBoundsException + * if the string is not hex formatted. + */ + public static final int parseHexInt32(final byte[] bs, final int p) { + int r = digits16[bs[p]] << 4; + + r |= digits16[bs[p + 1]]; + r <<= 4; + + r |= digits16[bs[p + 2]]; + r <<= 4; + + r |= digits16[bs[p + 3]]; + r <<= 4; + + r |= digits16[bs[p + 4]]; + r <<= 4; + + r |= digits16[bs[p + 5]]; + r <<= 4; + + r |= digits16[bs[p + 6]]; + + final int last = digits16[bs[p + 7]]; + if (r < 0 || last < 0) + throw new ArrayIndexOutOfBoundsException(); + return (r << 4) | last; + } + + /** + * Parse a single hex digit to its numeric value (0-15). + * + * @param digit + * hex character to parse. + * @return numeric value, in the range 0-15. + * @throws ArrayIndexOutOfBoundsException + * if the input digit is not a valid hex digit. + */ + public static final int parseHexInt4(final byte digit) { + final byte r = digits16[digit]; + if (r < 0) + throw new ArrayIndexOutOfBoundsException(); + return r; + } + + /** + * Parse a Git style timezone string. + * <p> + * The sequence "-0315" will be parsed as the numeric value -195, as the + * lower two positions count minutes, not 100ths of an hour. + * + * @param b + * buffer to scan. + * @param ptr + * position within buffer to start parsing digits at. + * @return the timezone at this location, expressed in minutes. + */ + public static final int parseTimeZoneOffset(final byte[] b, int ptr) { + final int v = parseBase10(b, ptr, null); + final int tzMins = v % 100; + final int tzHours = v / 100; + return tzHours * 60 + tzMins; + } + + /** + * Locate the first position after a given character. + * + * @param b + * buffer to scan. + * @param ptr + * position within buffer to start looking for chrA at. + * @param chrA + * character to find. + * @return new position just after chrA. + */ + public static final int next(final byte[] b, int ptr, final char chrA) { + final int sz = b.length; + while (ptr < sz) { + if (b[ptr++] == chrA) + return ptr; + } + return ptr; + } + + /** + * Locate the first position after the next LF. + * <p> + * This method stops on the first '\n' it finds. + * + * @param b + * buffer to scan. + * @param ptr + * position within buffer to start looking for LF at. + * @return new position just after the first LF found. + */ + public static final int nextLF(final byte[] b, int ptr) { + return next(b, ptr, '\n'); + } + + /** + * Locate the first position after either the given character or LF. + * <p> + * This method stops on the first match it finds from either chrA or '\n'. + * + * @param b + * buffer to scan. + * @param ptr + * position within buffer to start looking for chrA or LF at. + * @param chrA + * character to find. + * @return new position just after the first chrA or LF to be found. + */ + public static final int nextLF(final byte[] b, int ptr, final char chrA) { + final int sz = b.length; + while (ptr < sz) { + final byte c = b[ptr++]; + if (c == chrA || c == '\n') + return ptr; + } + return ptr; + } + + /** + * Locate the first position before a given character. + * + * @param b + * buffer to scan. + * @param ptr + * position within buffer to start looking for chrA at. + * @param chrA + * character to find. + * @return new position just before chrA, -1 for not found + */ + public static final int prev(final byte[] b, int ptr, final char chrA) { + if (ptr == b.length) + --ptr; + while (ptr >= 0) { + if (b[ptr--] == chrA) + return ptr; + } + return ptr; + } + + /** + * Locate the first position before the previous LF. + * <p> + * This method stops on the first '\n' it finds. + * + * @param b + * buffer to scan. + * @param ptr + * position within buffer to start looking for LF at. + * @return new position just before the first LF found, -1 for not found + */ + public static final int prevLF(final byte[] b, int ptr) { + return prev(b, ptr, '\n'); + } + + /** + * Locate the previous position before either the given character or LF. + * <p> + * This method stops on the first match it finds from either chrA or '\n'. + * + * @param b + * buffer to scan. + * @param ptr + * position within buffer to start looking for chrA or LF at. + * @param chrA + * character to find. + * @return new position just before the first chrA or LF to be found, -1 for + * not found + */ + public static final int prevLF(final byte[] b, int ptr, final char chrA) { + if (ptr == b.length) + --ptr; + while (ptr >= 0) { + final byte c = b[ptr--]; + if (c == chrA || c == '\n') + return ptr; + } + return ptr; + } + + /** + * Index the region between <code>[ptr, end)</code> to find line starts. + * <p> + * The returned list is 1 indexed. Index 0 contains + * {@link Integer#MIN_VALUE} to pad the list out. + * <p> + * Using a 1 indexed list means that line numbers can be directly accessed + * from the list, so <code>list.get(1)</code> (aka get line 1) returns + * <code>ptr</code>. + * <p> + * The last element (index <code>map.size()-1</code>) always contains + * <code>end</code>. + * + * @param buf + * buffer to scan. + * @param ptr + * position within the buffer corresponding to the first byte of + * line 1. + * @param end + * 1 past the end of the content within <code>buf</code>. + * @return a line map indexing the start position of each line. + */ + public static final IntList lineMap(final byte[] buf, int ptr, int end) { + // Experimentally derived from multiple source repositories + // the average number of bytes/line is 36. Its a rough guess + // to initially size our map close to the target. + // + final IntList map = new IntList((end - ptr) / 36); + map.fillTo(1, Integer.MIN_VALUE); + for (; ptr < end; ptr = nextLF(buf, ptr)) + map.add(ptr); + map.add(end); + return map; + } + + /** + * Locate the "author " header line data. + * + * @param b + * buffer to scan. + * @param ptr + * position in buffer to start the scan at. Most callers should + * pass 0 to ensure the scan starts from the beginning of the + * commit buffer and does not accidentally look at message body. + * @return position just after the space in "author ", so the first + * character of the author's name. If no author header can be + * located -1 is returned. + */ + public static final int author(final byte[] b, int ptr) { + final int sz = b.length; + if (ptr == 0) + ptr += 46; // skip the "tree ..." line. + while (ptr < sz && b[ptr] == 'p') + ptr += 48; // skip this parent. + return match(b, ptr, author); + } + + /** + * Locate the "committer " header line data. + * + * @param b + * buffer to scan. + * @param ptr + * position in buffer to start the scan at. Most callers should + * pass 0 to ensure the scan starts from the beginning of the + * commit buffer and does not accidentally look at message body. + * @return position just after the space in "committer ", so the first + * character of the committer's name. If no committer header can be + * located -1 is returned. + */ + public static final int committer(final byte[] b, int ptr) { + final int sz = b.length; + if (ptr == 0) + ptr += 46; // skip the "tree ..." line. + while (ptr < sz && b[ptr] == 'p') + ptr += 48; // skip this parent. + if (ptr < sz && b[ptr] == 'a') + ptr = nextLF(b, ptr); + return match(b, ptr, committer); + } + + /** + * Locate the "tagger " header line data. + * + * @param b + * buffer to scan. + * @param ptr + * position in buffer to start the scan at. Most callers should + * pass 0 to ensure the scan starts from the beginning of the tag + * buffer and does not accidentally look at message body. + * @return position just after the space in "tagger ", so the first + * character of the tagger's name. If no tagger header can be + * located -1 is returned. + */ + public static final int tagger(final byte[] b, int ptr) { + final int sz = b.length; + if (ptr == 0) + ptr += 48; // skip the "object ..." line. + while (ptr < sz) { + if (b[ptr] == '\n') + return -1; + final int m = match(b, ptr, tagger); + if (m >= 0) + return m; + ptr = nextLF(b, ptr); + } + return -1; + } + + /** + * Locate the "encoding " header line. + * + * @param b + * buffer to scan. + * @param ptr + * position in buffer to start the scan at. Most callers should + * pass 0 to ensure the scan starts from the beginning of the + * buffer and does not accidentally look at the message body. + * @return position just after the space in "encoding ", so the first + * character of the encoding's name. If no encoding header can be + * located -1 is returned (and UTF-8 should be assumed). + */ + public static final int encoding(final byte[] b, int ptr) { + final int sz = b.length; + while (ptr < sz) { + if (b[ptr] == '\n') + return -1; + if (b[ptr] == 'e') + break; + ptr = nextLF(b, ptr); + } + return match(b, ptr, encoding); + } + + /** + * Parse the "encoding " header into a character set reference. + * <p> + * Locates the "encoding " header (if present) by first calling + * {@link #encoding(byte[], int)} and then returns the proper character set + * to apply to this buffer to evaluate its contents as character data. + * <p> + * If no encoding header is present, {@link Constants#CHARSET} is assumed. + * + * @param b + * buffer to scan. + * @return the Java character set representation. Never null. + */ + public static Charset parseEncoding(final byte[] b) { + final int enc = encoding(b, 0); + if (enc < 0) + return Constants.CHARSET; + final int lf = nextLF(b, enc); + return Charset.forName(decode(Constants.CHARSET, b, enc, lf - 1)); + } + + /** + * Parse a name line (e.g. author, committer, tagger) into a PersonIdent. + * <p> + * When passing in a value for <code>nameB</code> callers should use the + * return value of {@link #author(byte[], int)} or + * {@link #committer(byte[], int)}, as these methods provide the proper + * position within the buffer. + * + * @param raw + * the buffer to parse character data from. + * @param nameB + * first position of the identity information. This should be the + * first position after the space which delimits the header field + * name (e.g. "author" or "committer") from the rest of the + * identity line. + * @return the parsed identity. Never null. + */ + public static PersonIdent parsePersonIdent(final byte[] raw, final int nameB) { + final Charset cs = parseEncoding(raw); + final int emailB = nextLF(raw, nameB, '<'); + final int emailE = nextLF(raw, emailB, '>'); + + final String name = decode(cs, raw, nameB, emailB - 2); + final String email = decode(cs, raw, emailB, emailE - 1); + + final MutableInteger ptrout = new MutableInteger(); + final long when = parseLongBase10(raw, emailE + 1, ptrout); + final int tz = parseTimeZoneOffset(raw, ptrout.value); + + return new PersonIdent(name, email, when * 1000L, tz); + } + + /** + * Parse a name data (e.g. as within a reflog) into a PersonIdent. + * <p> + * When passing in a value for <code>nameB</code> callers should use the + * return value of {@link #author(byte[], int)} or + * {@link #committer(byte[], int)}, as these methods provide the proper + * position within the buffer. + * + * @param raw + * the buffer to parse character data from. + * @param nameB + * first position of the identity information. This should be the + * first position after the space which delimits the header field + * name (e.g. "author" or "committer") from the rest of the + * identity line. + * @return the parsed identity. Never null. + */ + public static PersonIdent parsePersonIdentOnly(final byte[] raw, final int nameB) { + int stop = nextLF(raw, nameB); + int emailB = nextLF(raw, nameB, '<'); + int emailE = nextLF(raw, emailB, '>'); + final String name; + final String email; + if (emailE < stop) { + email = decode(raw, emailB, emailE - 1); + } else { + email = "invalid"; + } + if (emailB < stop) + name = decode(raw, nameB, emailB - 2); + else + name = decode(raw, nameB, stop); + + final MutableInteger ptrout = new MutableInteger(); + long when; + int tz; + if (emailE < stop) { + when = parseLongBase10(raw, emailE + 1, ptrout); + tz = parseTimeZoneOffset(raw, ptrout.value); + } else { + when = 0; + tz = 0; + } + return new PersonIdent(name, email, when * 1000L, tz); + } + + /** + * Locate the end of a footer line key string. + * <p> + * If the region at {@code raw[ptr]} matches {@code ^[A-Za-z0-9-]+:} (e.g. + * "Signed-off-by: A. U. Thor\n") then this method returns the position of + * the first ':'. + * <p> + * If the region at {@code raw[ptr]} does not match {@code ^[A-Za-z0-9-]+:} + * then this method returns -1. + * + * @param raw + * buffer to scan. + * @param ptr + * first position within raw to consider as a footer line key. + * @return position of the ':' which terminates the footer line key if this + * is otherwise a valid footer line key; otherwise -1. + */ + public static int endOfFooterLineKey(final byte[] raw, int ptr) { + try { + for (;;) { + final byte c = raw[ptr]; + if (footerLineKeyChars[c] == 0) { + if (c == ':') + return ptr; + return -1; + } + ptr++; + } + } catch (ArrayIndexOutOfBoundsException e) { + return -1; + } + } + + /** + * Decode a buffer under UTF-8, if possible. + * + * If the byte stream cannot be decoded that way, the platform default is tried + * and if that too fails, the fail-safe ISO-8859-1 encoding is tried. + * + * @param buffer + * buffer to pull raw bytes from. + * @return a string representation of the range <code>[start,end)</code>, + * after decoding the region through the specified character set. + */ + public static String decode(final byte[] buffer) { + return decode(buffer, 0, buffer.length); + } + + /** + * Decode a buffer under UTF-8, if possible. + * + * If the byte stream cannot be decoded that way, the platform default is + * tried and if that too fails, the fail-safe ISO-8859-1 encoding is tried. + * + * @param buffer + * buffer to pull raw bytes from. + * @param start + * start position in buffer + * @param end + * one position past the last location within the buffer to take + * data from. + * @return a string representation of the range <code>[start,end)</code>, + * after decoding the region through the specified character set. + */ + public static String decode(final byte[] buffer, final int start, + final int end) { + return decode(Constants.CHARSET, buffer, start, end); + } + + /** + * Decode a buffer under the specified character set if possible. + * + * If the byte stream cannot be decoded that way, the platform default is tried + * and if that too fails, the fail-safe ISO-8859-1 encoding is tried. + * + * @param cs + * character set to use when decoding the buffer. + * @param buffer + * buffer to pull raw bytes from. + * @return a string representation of the range <code>[start,end)</code>, + * after decoding the region through the specified character set. + */ + public static String decode(final Charset cs, final byte[] buffer) { + return decode(cs, buffer, 0, buffer.length); + } + + /** + * Decode a region of the buffer under the specified character set if possible. + * + * If the byte stream cannot be decoded that way, the platform default is tried + * and if that too fails, the fail-safe ISO-8859-1 encoding is tried. + * + * @param cs + * character set to use when decoding the buffer. + * @param buffer + * buffer to pull raw bytes from. + * @param start + * first position within the buffer to take data from. + * @param end + * one position past the last location within the buffer to take + * data from. + * @return a string representation of the range <code>[start,end)</code>, + * after decoding the region through the specified character set. + */ + public static String decode(final Charset cs, final byte[] buffer, + final int start, final int end) { + try { + return decodeNoFallback(cs, buffer, start, end); + } catch (CharacterCodingException e) { + // Fall back to an ISO-8859-1 style encoding. At least all of + // the bytes will be present in the output. + // + return extractBinaryString(buffer, start, end); + } + } + + /** + * Decode a region of the buffer under the specified character set if + * possible. + * + * If the byte stream cannot be decoded that way, the platform default is + * tried and if that too fails, an exception is thrown. + * + * @param cs + * character set to use when decoding the buffer. + * @param buffer + * buffer to pull raw bytes from. + * @param start + * first position within the buffer to take data from. + * @param end + * one position past the last location within the buffer to take + * data from. + * @return a string representation of the range <code>[start,end)</code>, + * after decoding the region through the specified character set. + * @throws CharacterCodingException + * the input is not in any of the tested character sets. + */ + public static String decodeNoFallback(final Charset cs, + final byte[] buffer, final int start, final int end) + throws CharacterCodingException { + final ByteBuffer b = ByteBuffer.wrap(buffer, start, end - start); + b.mark(); + + // Try our built-in favorite. The assumption here is that + // decoding will fail if the data is not actually encoded + // using that encoder. + // + try { + return decode(b, Constants.CHARSET); + } catch (CharacterCodingException e) { + b.reset(); + } + + if (!cs.equals(Constants.CHARSET)) { + // Try the suggested encoding, it might be right since it was + // provided by the caller. + // + try { + return decode(b, cs); + } catch (CharacterCodingException e) { + b.reset(); + } + } + + // Try the default character set. A small group of people + // might actually use the same (or very similar) locale. + // + final Charset defcs = Charset.defaultCharset(); + if (!defcs.equals(cs) && !defcs.equals(Constants.CHARSET)) { + try { + return decode(b, defcs); + } catch (CharacterCodingException e) { + b.reset(); + } + } + + throw new CharacterCodingException(); + } + + /** + * Decode a region of the buffer under the ISO-8859-1 encoding. + * + * Each byte is treated as a single character in the 8859-1 character + * encoding, performing a raw binary->char conversion. + * + * @param buffer + * buffer to pull raw bytes from. + * @param start + * first position within the buffer to take data from. + * @param end + * one position past the last location within the buffer to take + * data from. + * @return a string representation of the range <code>[start,end)</code>. + */ + public static String extractBinaryString(final byte[] buffer, + final int start, final int end) { + final StringBuilder r = new StringBuilder(end - start); + for (int i = start; i < end; i++) + r.append((char) (buffer[i] & 0xff)); + return r.toString(); + } + + private static String decode(final ByteBuffer b, final Charset charset) + throws CharacterCodingException { + final CharsetDecoder d = charset.newDecoder(); + d.onMalformedInput(CodingErrorAction.REPORT); + d.onUnmappableCharacter(CodingErrorAction.REPORT); + return d.decode(b).toString(); + } + + /** + * Locate the position of the commit message body. + * + * @param b + * buffer to scan. + * @param ptr + * position in buffer to start the scan at. Most callers should + * pass 0 to ensure the scan starts from the beginning of the + * commit buffer. + * @return position of the user's message buffer. + */ + public static final int commitMessage(final byte[] b, int ptr) { + final int sz = b.length; + if (ptr == 0) + ptr += 46; // skip the "tree ..." line. + while (ptr < sz && b[ptr] == 'p') + ptr += 48; // skip this parent. + + // Skip any remaining header lines, ignoring what their actual + // header line type is. This is identical to the logic for a tag. + // + return tagMessage(b, ptr); + } + + /** + * Locate the position of the tag message body. + * + * @param b + * buffer to scan. + * @param ptr + * position in buffer to start the scan at. Most callers should + * pass 0 to ensure the scan starts from the beginning of the tag + * buffer. + * @return position of the user's message buffer. + */ + public static final int tagMessage(final byte[] b, int ptr) { + final int sz = b.length; + if (ptr == 0) + ptr += 48; // skip the "object ..." line. + while (ptr < sz && b[ptr] != '\n') + ptr = nextLF(b, ptr); + if (ptr < sz && b[ptr] == '\n') + return ptr + 1; + return -1; + } + + /** + * Locate the end of a paragraph. + * <p> + * A paragraph is ended by two consecutive LF bytes. + * + * @param b + * buffer to scan. + * @param start + * position in buffer to start the scan at. Most callers will + * want to pass the first position of the commit message (as + * found by {@link #commitMessage(byte[], int)}. + * @return position of the LF at the end of the paragraph; + * <code>b.length</code> if no paragraph end could be located. + */ + public static final int endOfParagraph(final byte[] b, final int start) { + int ptr = start; + final int sz = b.length; + while (ptr < sz && b[ptr] != '\n') + ptr = nextLF(b, ptr); + while (0 < ptr && start < ptr && b[ptr - 1] == '\n') + ptr--; + return ptr; + } + + private RawParseUtils() { + // Don't create instances of a static only utility. + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawSubStringPattern.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawSubStringPattern.java new file mode 100644 index 0000000000..ae135afab7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawSubStringPattern.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +import org.eclipse.jgit.lib.Constants; + +/** + * Searches text using only substring search. + * <p> + * Instances are thread-safe. Multiple concurrent threads may perform matches on + * different character sequences at the same time. + */ +public class RawSubStringPattern { + private final String needleString; + + private final byte[] needle; + + /** + * Construct a new substring pattern. + * + * @param patternText + * text to locate. This should be a literal string, as no + * meta-characters are supported by this implementation. The + * string may not be the empty string. + */ + public RawSubStringPattern(final String patternText) { + if (patternText.length() == 0) + throw new IllegalArgumentException("Cannot match on empty string."); + needleString = patternText; + + final byte[] b = Constants.encode(patternText); + needle = new byte[b.length]; + for (int i = 0; i < b.length; i++) + needle[i] = lc(b[i]); + } + + /** + * Match a character sequence against this pattern. + * + * @param rcs + * the sequence to match. Must not be null but the length of the + * sequence is permitted to be 0. + * @return offset within <code>rcs</code> of the first occurrence of this + * pattern; -1 if this pattern does not appear at any position of + * <code>rcs</code>. + */ + public int match(final RawCharSequence rcs) { + final int needleLen = needle.length; + final byte first = needle[0]; + + final byte[] text = rcs.buffer; + int matchPos = rcs.startPtr; + final int maxPos = rcs.endPtr - needleLen; + + OUTER: for (; matchPos < maxPos; matchPos++) { + if (neq(first, text[matchPos])) { + while (++matchPos < maxPos && neq(first, text[matchPos])) { + /* skip */ + } + if (matchPos == maxPos) + return -1; + } + + int si = ++matchPos; + for (int j = 1; j < needleLen; j++, si++) { + if (neq(needle[j], text[si])) + continue OUTER; + } + return matchPos - 1; + } + return -1; + } + + private static final boolean neq(final byte a, final byte b) { + return a != b && a != lc(b); + } + + private static final byte lc(final byte q) { + return (byte) StringUtils.toLowerCase((char) (q & 0xff)); + } + + /** + * Get the literal pattern string this instance searches for. + * + * @return the pattern string given to our constructor. + */ + public String pattern() { + return needleString; + } + + @Override + public String toString() { + return pattern(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java new file mode 100644 index 0000000000..91f03f095e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +/** Miscellaneous string comparison utility methods. */ +public final class StringUtils { + private static final char[] LC; + + static { + LC = new char['Z' + 1]; + for (char c = 0; c < LC.length; c++) + LC[c] = c; + for (char c = 'A'; c <= 'Z'; c++) + LC[c] = (char) ('a' + (c - 'A')); + } + + /** + * Convert the input to lowercase. + * <p> + * This method does not honor the JVM locale, but instead always behaves as + * though it is in the US-ASCII locale. Only characters in the range 'A' + * through 'Z' are converted. All other characters are left as-is, even if + * they otherwise would have a lowercase character equivilant. + * + * @param c + * the input character. + * @return lowercase version of the input. + */ + public static char toLowerCase(final char c) { + return c <= 'Z' ? LC[c] : c; + } + + /** + * Convert the input string to lower case, according to the "C" locale. + * <p> + * This method does not honor the JVM locale, but instead always behaves as + * though it is in the US-ASCII locale. Only characters in the range 'A' + * through 'Z' are converted, all other characters are left as-is, even if + * they otherwise would have a lowercase character equivilant. + * + * @param in + * the input string. Must not be null. + * @return a copy of the input string, after converting characters in the + * range 'A'..'Z' to 'a'..'z'. + */ + public static String toLowerCase(final String in) { + final StringBuilder r = new StringBuilder(in.length()); + for (int i = 0; i < in.length(); i++) + r.append(toLowerCase(in.charAt(i))); + return r.toString(); + } + + /** + * Test if two strings are equal, ignoring case. + * <p> + * This method does not honor the JVM locale, but instead always behaves as + * though it is in the US-ASCII locale. + * + * @param a + * first string to compare. + * @param b + * second string to compare. + * @return true if a equals b + */ + public static boolean equalsIgnoreCase(final String a, final String b) { + if (a == b) + return true; + if (a.length() != b.length()) + return false; + for (int i = 0; i < a.length(); i++) { + if (toLowerCase(a.charAt(i)) != toLowerCase(b.charAt(i))) + return false; + } + return true; + } + + private StringUtils() { + // Do not create instances + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java new file mode 100644 index 0000000000..771e77058a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2009, Google Inc. + * Copyright (C) 2009, Robin Rosenberg <robin.rosenberg@dewire.com> + * Copyright (C) 2009, Yann Simon <yann.simon.fr@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +import java.io.File; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.TimeZone; + +import org.eclipse.jgit.lib.FileBasedConfig; + +/** + * Interface to read values from the system. + * <p> + * When writing unit tests, extending this interface with a custom class + * permits to simulate an access to a system variable or property and + * permits to control the user's global configuration. + * </p> + */ +public abstract class SystemReader { + private static SystemReader INSTANCE = new SystemReader() { + private volatile String hostname; + + public String getenv(String variable) { + return System.getenv(variable); + } + + public String getProperty(String key) { + return System.getProperty(key); + } + + public FileBasedConfig openUserConfig() { + final File home = FS.userHome(); + return new FileBasedConfig(new File(home, ".gitconfig")); + } + + public String getHostname() { + if (hostname == null) { + try { + InetAddress localMachine = InetAddress.getLocalHost(); + hostname = localMachine.getCanonicalHostName(); + } catch (UnknownHostException e) { + // we do nothing + hostname = "localhost"; + } + assert hostname != null; + } + return hostname; + } + + @Override + public long getCurrentTime() { + return System.currentTimeMillis(); + } + + @Override + public int getTimezone(long when) { + return TimeZone.getDefault().getOffset(when) / (60 * 1000); + } + }; + + /** @return the live instance to read system properties. */ + public static SystemReader getInstance() { + return INSTANCE; + } + + /** + * @param newReader + * the new instance to use when accessing properties. + */ + public static void setInstance(SystemReader newReader) { + INSTANCE = newReader; + } + + /** + * Gets the hostname of the local host. If no hostname can be found, the + * hostname is set to the default value "localhost". + * + * @return the canonical hostname + */ + public abstract String getHostname(); + + /** + * @param variable system variable to read + * @return value of the system variable + */ + public abstract String getenv(String variable); + + /** + * @param key of the system property to read + * @return value of the system property + */ + public abstract String getProperty(String key); + + /** + * @return the git configuration found in the user home + */ + public abstract FileBasedConfig openUserConfig(); + + /** + * @return the current system time + */ + public abstract long getCurrentTime(); + + /** + * @param when TODO + * @return the local time zone + */ + public abstract int getTimezone(long when); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java new file mode 100644 index 0000000000..9c6addebd8 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/TemporaryBuffer.java @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; + +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ProgressMonitor; + +/** + * A fully buffered output stream using local disk storage for large data. + * <p> + * Initially this output stream buffers to memory, like ByteArrayOutputStream + * might do, but it shifts to using an on disk temporary file if the output gets + * too large. + * <p> + * The content of this buffered stream may be sent to another OutputStream only + * after this stream has been properly closed by {@link #close()}. + */ +public class TemporaryBuffer extends OutputStream { + static final int DEFAULT_IN_CORE_LIMIT = 1024 * 1024; + + /** Chain of data, if we are still completely in-core; otherwise null. */ + private ArrayList<Block> blocks; + + /** + * Maximum number of bytes we will permit storing in memory. + * <p> + * When this limit is reached the data will be shifted to a file on disk, + * preventing the JVM heap from growing out of control. + */ + private int inCoreLimit; + + /** + * Location of our temporary file if we are on disk; otherwise null. + * <p> + * If we exceeded the {@link #inCoreLimit} we nulled out {@link #blocks} and + * created this file instead. All output goes here through {@link #diskOut}. + */ + private File onDiskFile; + + /** If writing to {@link #onDiskFile} this is a buffered stream to it. */ + private OutputStream diskOut; + + /** Create a new empty temporary buffer. */ + public TemporaryBuffer() { + inCoreLimit = DEFAULT_IN_CORE_LIMIT; + blocks = new ArrayList<Block>(inCoreLimit / Block.SZ); + blocks.add(new Block()); + } + + @Override + public void write(final int b) throws IOException { + if (blocks == null) { + diskOut.write(b); + return; + } + + Block s = last(); + if (s.isFull()) { + if (reachedInCoreLimit()) { + diskOut.write(b); + return; + } + + s = new Block(); + blocks.add(s); + } + s.buffer[s.count++] = (byte) b; + } + + @Override + public void write(final byte[] b, int off, int len) throws IOException { + if (blocks != null) { + while (len > 0) { + Block s = last(); + if (s.isFull()) { + if (reachedInCoreLimit()) + break; + + s = new Block(); + blocks.add(s); + } + + final int n = Math.min(Block.SZ - s.count, len); + System.arraycopy(b, off, s.buffer, s.count, n); + s.count += n; + len -= n; + off += n; + } + } + + if (len > 0) + diskOut.write(b, off, len); + } + + /** + * Copy all bytes remaining on the input stream into this buffer. + * + * @param in + * the stream to read from, until EOF is reached. + * @throws IOException + * an error occurred reading from the input stream, or while + * writing to a local temporary file. + */ + public void copy(final InputStream in) throws IOException { + if (blocks != null) { + for (;;) { + Block s = last(); + if (s.isFull()) { + if (reachedInCoreLimit()) + break; + s = new Block(); + blocks.add(s); + } + + final int n = in.read(s.buffer, s.count, Block.SZ - s.count); + if (n < 1) + return; + s.count += n; + } + } + + final byte[] tmp = new byte[Block.SZ]; + int n; + while ((n = in.read(tmp)) > 0) + diskOut.write(tmp, 0, n); + } + + private Block last() { + return blocks.get(blocks.size() - 1); + } + + private boolean reachedInCoreLimit() throws IOException { + if (blocks.size() * Block.SZ < inCoreLimit) + return false; + + onDiskFile = File.createTempFile("jgit_", ".buffer"); + diskOut = new FileOutputStream(onDiskFile); + + final Block last = blocks.remove(blocks.size() - 1); + for (final Block b : blocks) + diskOut.write(b.buffer, 0, b.count); + blocks = null; + + diskOut = new BufferedOutputStream(diskOut, Block.SZ); + diskOut.write(last.buffer, 0, last.count); + return true; + } + + public void close() throws IOException { + if (diskOut != null) { + try { + diskOut.close(); + } finally { + diskOut = null; + } + } + } + + /** + * Obtain the length (in bytes) of the buffer. + * <p> + * The length is only accurate after {@link #close()} has been invoked. + * + * @return total length of the buffer, in bytes. + */ + public long length() { + if (onDiskFile != null) + return onDiskFile.length(); + + final Block last = last(); + return ((long) blocks.size()) * Block.SZ - (Block.SZ - last.count); + } + + /** + * Convert this buffer's contents into a contiguous byte array. + * <p> + * The buffer is only complete after {@link #close()} has been invoked. + * + * @return the complete byte array; length matches {@link #length()}. + * @throws IOException + * an error occurred reading from a local temporary file + * @throws OutOfMemoryError + * the buffer cannot fit in memory + */ + public byte[] toByteArray() throws IOException { + final long len = length(); + if (Integer.MAX_VALUE < len) + throw new OutOfMemoryError("Length exceeds maximum array size"); + + final byte[] out = new byte[(int) len]; + if (blocks != null) { + int outPtr = 0; + for (final Block b : blocks) { + System.arraycopy(b.buffer, 0, out, outPtr, b.count); + outPtr += b.count; + } + } else { + final FileInputStream in = new FileInputStream(onDiskFile); + try { + NB.readFully(in, out, 0, (int) len); + } finally { + in.close(); + } + } + return out; + } + + /** + * Send this buffer to an output stream. + * <p> + * This method may only be invoked after {@link #close()} has completed + * normally, to ensure all data is completely transferred. + * + * @param os + * stream to send this buffer's complete content to. + * @param pm + * if not null progress updates are sent here. Caller should + * initialize the task and the number of work units to + * <code>{@link #length()}/1024</code>. + * @throws IOException + * an error occurred reading from a temporary file on the local + * system, or writing to the output stream. + */ + public void writeTo(final OutputStream os, ProgressMonitor pm) + throws IOException { + if (pm == null) + pm = NullProgressMonitor.INSTANCE; + if (blocks != null) { + // Everything is in core so we can stream directly to the output. + // + for (final Block b : blocks) { + os.write(b.buffer, 0, b.count); + pm.update(b.count / 1024); + } + } else { + // Reopen the temporary file and copy the contents. + // + final FileInputStream in = new FileInputStream(onDiskFile); + try { + int cnt; + final byte[] buf = new byte[Block.SZ]; + while ((cnt = in.read(buf)) >= 0) { + os.write(buf, 0, cnt); + pm.update(cnt / 1024); + } + } finally { + in.close(); + } + } + } + + /** Clear this buffer so it has no data, and cannot be used again. */ + public void destroy() { + blocks = null; + + if (diskOut != null) { + try { + diskOut.close(); + } catch (IOException err) { + // We shouldn't encounter an error closing the file. + } finally { + diskOut = null; + } + } + + if (onDiskFile != null) { + if (!onDiskFile.delete()) + onDiskFile.deleteOnExit(); + onDiskFile = null; + } + } + + static class Block { + static final int SZ = 8 * 1024; + + final byte[] buffer = new byte[SZ]; + + int count; + + boolean isFull() { + return count == SZ; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/InterruptTimer.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/InterruptTimer.java new file mode 100644 index 0000000000..91aa1cb6d2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/InterruptTimer.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util.io; + +/** + * Triggers an interrupt on the calling thread if it doesn't complete a block. + * <p> + * Classes can use this to trip an alarm interrupting the calling thread if it + * doesn't complete a block within the specified timeout. Typical calling + * pattern is: + * + * <pre> + * private InterruptTimer myTimer = ...; + * void foo() { + * try { + * myTimer.begin(timeout); + * // work + * } finally { + * myTimer.end(); + * } + * } + * </pre> + * <p> + * An InterruptTimer is not recursive. To implement recursive timers, + * independent InterruptTimer instances are required. A single InterruptTimer + * may be shared between objects which won't recursively call each other. + * <p> + * Each InterruptTimer spawns one background thread to sleep the specified time + * and interrupt the thread which called {@link #begin(int)}. It is up to the + * caller to ensure that the operations within the work block between the + * matched begin and end calls tests the interrupt flag (most IO operations do). + * <p> + * To terminate the background thread, use {@link #terminate()}. If the + * application fails to terminate the thread, it will (eventually) terminate + * itself when the InterruptTimer instance is garbage collected. + * + * @see TimeoutInputStream + */ +public final class InterruptTimer { + private final AlarmState state; + + private final AlarmThread thread; + + final AutoKiller autoKiller; + + /** Create a new timer with a default thread name. */ + public InterruptTimer() { + this("JGit-InterruptTimer"); + } + + /** + * Create a new timer to signal on interrupt on the caller. + * <p> + * The timer thread is created in the calling thread's ThreadGroup. + * + * @param threadName + * name of the timer thread. + */ + public InterruptTimer(final String threadName) { + state = new AlarmState(); + autoKiller = new AutoKiller(state); + thread = new AlarmThread(threadName, state); + thread.start(); + } + + /** + * Arm the interrupt timer before entering a blocking operation. + * + * @param timeout + * number of milliseconds before the interrupt should trigger. + * Must be > 0. + */ + public void begin(final int timeout) { + if (timeout <= 0) + throw new IllegalArgumentException("Invalid timeout: " + timeout); + Thread.interrupted(); + state.begin(timeout); + } + + /** Disable the interrupt timer, as the operation is complete. */ + public void end() { + state.end(); + } + + /** Shutdown the timer thread, and wait for it to terminate. */ + public void terminate() { + state.terminate(); + try { + thread.join(); + } catch (InterruptedException e) { + // + } + } + + static final class AlarmThread extends Thread { + AlarmThread(final String name, final AlarmState q) { + super(q); + setName(name); + setDaemon(true); + } + } + + // The trick here is, the AlarmThread does not have a reference to the + // AutoKiller instance, only the InterruptTimer itself does. Thus when + // the InterruptTimer is GC'd, the AutoKiller is also unreachable and + // can be GC'd. When it gets finalized, it tells the AlarmThread to + // terminate, triggering the thread to exit gracefully. + // + private static final class AutoKiller { + private final AlarmState state; + + AutoKiller(final AlarmState s) { + state = s; + } + + @Override + protected void finalize() throws Throwable { + state.terminate(); + } + } + + static final class AlarmState implements Runnable { + private Thread callingThread; + + private long deadline; + + private boolean terminated; + + AlarmState() { + callingThread = Thread.currentThread(); + } + + public synchronized void run() { + while (!terminated && callingThread.isAlive()) { + try { + if (0 < deadline) { + final long delay = deadline - now(); + if (delay <= 0) { + deadline = 0; + callingThread.interrupt(); + } else { + wait(delay); + } + } else { + wait(1000); + } + } catch (InterruptedException e) { + // Treat an interrupt as notice to examine state. + } + } + } + + synchronized void begin(final int timeout) { + if (terminated) + throw new IllegalStateException("Timer already terminated"); + callingThread = Thread.currentThread(); + deadline = now() + timeout; + notifyAll(); + } + + synchronized void end() { + if (0 == deadline) + Thread.interrupted(); + else + deadline = 0; + notifyAll(); + } + + synchronized void terminate() { + if (!terminated) { + deadline = 0; + terminated = true; + notifyAll(); + } + } + + private static long now() { + return System.currentTimeMillis(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutInputStream.java new file mode 100644 index 0000000000..19d7933e1b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutInputStream.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; + +/** InputStream with a configurable timeout. */ +public class TimeoutInputStream extends FilterInputStream { + private final InterruptTimer myTimer; + + private int timeout; + + /** + * Wrap an input stream with a timeout on all read operations. + * + * @param src + * base input stream (to read from). The stream must be + * interruptible (most socket streams are). + * @param timer + * timer to manage the timeouts during reads. + */ + public TimeoutInputStream(final InputStream src, + final InterruptTimer timer) { + super(src); + myTimer = timer; + } + + /** @return number of milliseconds before aborting a read. */ + public int getTimeout() { + return timeout; + } + + /** + * @param millis + * number of milliseconds before aborting a read. Must be > 0. + */ + public void setTimeout(final int millis) { + if (millis < 0) + throw new IllegalArgumentException("Invalid timeout: " + millis); + timeout = millis; + } + + @Override + public int read() throws IOException { + try { + beginRead(); + return super.read(); + } catch (InterruptedIOException e) { + throw readTimedOut(); + } finally { + endRead(); + } + } + + @Override + public int read(byte[] buf) throws IOException { + return read(buf, 0, buf.length); + } + + @Override + public int read(byte[] buf, int off, int cnt) throws IOException { + try { + beginRead(); + return super.read(buf, off, cnt); + } catch (InterruptedIOException e) { + throw readTimedOut(); + } finally { + endRead(); + } + } + + @Override + public long skip(long cnt) throws IOException { + try { + beginRead(); + return super.skip(cnt); + } catch (InterruptedIOException e) { + throw readTimedOut(); + } finally { + endRead(); + } + } + + private void beginRead() { + myTimer.begin(timeout); + } + + private void endRead() { + myTimer.end(); + } + + private static InterruptedIOException readTimedOut() { + return new InterruptedIOException("Read timed out"); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutOutputStream.java new file mode 100644 index 0000000000..a826086cd1 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/TimeoutOutputStream.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ + +package org.eclipse.jgit.util.io; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.OutputStream; + +/** OutputStream with a configurable timeout. */ +public class TimeoutOutputStream extends OutputStream { + private final OutputStream dst; + + private final InterruptTimer myTimer; + + private int timeout; + + /** + * Wrap an output stream with a timeout on all write operations. + * + * @param destination + * base input stream (to write to). The stream must be + * interruptible (most socket streams are). + * @param timer + * timer to manage the timeouts during writes. + */ + public TimeoutOutputStream(final OutputStream destination, + final InterruptTimer timer) { + dst = destination; + myTimer = timer; + } + + /** @return number of milliseconds before aborting a write. */ + public int getTimeout() { + return timeout; + } + + /** + * @param millis + * number of milliseconds before aborting a write. Must be > 0. + */ + public void setTimeout(final int millis) { + if (millis < 0) + throw new IllegalArgumentException("Invalid timeout: " + millis); + timeout = millis; + } + + @Override + public void write(int b) throws IOException { + try { + beginWrite(); + dst.write(b); + } catch (InterruptedIOException e) { + throw writeTimedOut(); + } finally { + endWrite(); + } + } + + @Override + public void write(byte[] buf) throws IOException { + write(buf, 0, buf.length); + } + + @Override + public void write(byte[] buf, int off, int len) throws IOException { + try { + beginWrite(); + dst.write(buf, off, len); + } catch (InterruptedIOException e) { + throw writeTimedOut(); + } finally { + endWrite(); + } + } + + @Override + public void flush() throws IOException { + try { + beginWrite(); + dst.flush(); + } catch (InterruptedIOException e) { + throw writeTimedOut(); + } finally { + endWrite(); + } + } + + @Override + public void close() throws IOException { + try { + beginWrite(); + dst.close(); + } catch (InterruptedIOException e) { + throw writeTimedOut(); + } finally { + endWrite(); + } + } + + private void beginWrite() { + myTimer.begin(timeout); + } + + private void endWrite() { + myTimer.end(); + } + + private static InterruptedIOException writeTimedOut() { + return new InterruptedIOException("Write timed out"); + } +} |